diff --git a/examples/B0819W19WD/audio-previews/openai-alloy-preview.mp3 b/examples/B0819W19WD/audio-previews/openai-alloy-preview.mp3 new file mode 100644 index 0000000..151779d Binary files /dev/null and b/examples/B0819W19WD/audio-previews/openai-alloy-preview.mp3 differ diff --git a/examples/B0819W19WD/audio-previews/openai-alloy-preview.mp4 b/examples/B0819W19WD/audio-previews/openai-alloy-preview.mp4 new file mode 100644 index 0000000..62e3a05 Binary files /dev/null and b/examples/B0819W19WD/audio-previews/openai-alloy-preview.mp4 differ diff --git a/examples/B0819W19WD/audio-previews/openai-onyx-preview.mp3 b/examples/B0819W19WD/audio-previews/openai-onyx-preview.mp3 new file mode 100644 index 0000000..627930e Binary files /dev/null and b/examples/B0819W19WD/audio-previews/openai-onyx-preview.mp3 differ diff --git a/examples/B0819W19WD/audio-previews/openai-onyx-preview.mp4 b/examples/B0819W19WD/audio-previews/openai-onyx-preview.mp4 new file mode 100644 index 0000000..a0e67ce Binary files /dev/null and b/examples/B0819W19WD/audio-previews/openai-onyx-preview.mp4 differ diff --git a/examples/B0819W19WD/audio-previews/unrealspeech-scarlett-preview.mp3 b/examples/B0819W19WD/audio-previews/unrealspeech-scarlett-preview.mp3 new file mode 100644 index 0000000..72d4551 Binary files /dev/null and b/examples/B0819W19WD/audio-previews/unrealspeech-scarlett-preview.mp3 differ diff --git a/examples/B0819W19WD/audio-previews/unrealspeech-scarlett-preview.mp4 b/examples/B0819W19WD/audio-previews/unrealspeech-scarlett-preview.mp4 new file mode 100644 index 0000000..fcb9b06 Binary files /dev/null and b/examples/B0819W19WD/audio-previews/unrealspeech-scarlett-preview.mp4 differ diff --git a/examples/B0819W19WD/book-preview.md b/examples/B0819W19WD/book-preview.md new file mode 100644 index 0000000..8f39be4 --- /dev/null +++ b/examples/B0819W19WD/book-preview.md @@ -0,0 +1,39 @@ +# Revelation Space (The Inhibitor Trilogy) + +> By Alastair Reynolds + +**NOTE**: This is a **preview** containing only the first page for demo purposes. + +--- + +## Table of Contents + +- [Chapter 1](#chapter-1) + +--- + +## Chapter 1 + +Mantell Sector, North Nekhebet, Resurgam, Delta Pavonis system, 2551 + +There was a razorstorm coming in. + +Sylveste stood on the edge of the excavation and wondered if any of his labours would survive the night. The archaeological dig was an array of deep square shafts separated by baulks of sheer-sided soil: the classical Wheeler box-grid. The shafts went down tens of metres, walled by transparent cofferdams spun from hyperdiamond. A million years of stratified geological history pressed against the sheets. But it would take only one good dustfall—one good razorstorm—to fill the shafts almost to the surface. + +“Confirmation, sir,” said one of his team, emerging from the crouched form of the first crawler. The man’s voice was muffled behind his breather mask. “Cuvier’s just issued a severe weather advisory for the whole North Nekhbet landmass. They’re advising all surface teams to return to the nearest base.” + +“You’re saying we should pack up and drive back to Mantell?” + +“It’s going to be a hard one, sir.” The man fidgeted, drawing the collar of his jacket tighter around his neck. “Shall I issue the general evacuation order?” + +Sylveste looked down at the excavation grid, the sides of each shaft brightly lit by the banks of floodlights arrayed around the area. Pavonis never got high enough at these latitudes to provide much useful illumination; now, sinking towards the horizon and clotted by great cauls of dust, it was little more than a rusty-red smear, hard for his eyes to focus on. Soon dust devils would come, scurrying across the Ptero Steppes like so many overwound toy gyroscopes. Then the main thrust of the storm, rising like a black anvil. + +“No,” he said. “There’s no need for us to leave. We’re well sheltered here—there’s hardly any erosion pattering on those boulders, in case you hadn’t noticed. If the storm becomes too harsh, we’ll shelter in the crawlers.” + +The man looked at the rocks, shaking his head as if doubting the evidence of his ears. “Sir, Cuvier only issue an advisory of this severity once every year or two—it’s an order of magnitude above anything we’ve experienced before.” + +“Speak for yourself,” Sylveste said, noticing the way the man’s gaze snapped involuntarily to his eyes and then off again, embarrassed. “Listen to me. We cannot afford to abandon this dig. Do you understand?” + +--- + +_End of Preview_ diff --git a/package.json b/package.json index 263c728..fcbce89 100644 --- a/package.json +++ b/package.json @@ -18,26 +18,34 @@ "test": "run-s test:*", "test:format": "prettier --check \"**/*.{js,ts,tsx}\"", "test:lint": "eslint .", - "test:tyggpecheck": "tsc --noEmit", + "test:typecheck": "tsc --noEmit", "preinstall": "npx only-allow pnpm" }, "dependencies": { "@inquirer/prompts": "^7.0.0", "dotenv": "^16.4.5", + "fluent-ffmpeg": "^2.1.3", "globby": "^14.0.2", + "hash-object": "^5.0.1", + "hh-mm-ss": "^1.2.0", "kindle-api-ky": "^1.0.1", - "openai-fetch": "^3.2.0", + "ky": "^1.7.2", + "node-id3": "^0.2.6", + "openai-fetch": "^3.3.1", "p-map": "^7.0.2", "pdfkit": "^0.15.0", "playwright": "^1.47.2", - "playwright-core": "^1.47.2" + "playwright-core": "^1.47.2", + "unrealspeech-api": "^1.0.2" }, "devDependencies": { "@fisch0920/eslint-config": "^1.4.0", "@total-typescript/ts-reset": "^0.6.1", - "@types/node": "^22.4.0", + "@types/fluent-ffmpeg": "^2.1.26", + "@types/hh-mm-ss": "^1.2.3", + "@types/node": "^22.7.5", "@types/pdfkit": "^0.13.5", - "del-cli": "^5.1.0", + "del-cli": "^6.0.0", "delay": "^6.0.0", "eslint": "^8.57.0", "npm-run-all2": "^6.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a12b323..9ed0fe7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,19 +10,34 @@ importers: dependencies: '@inquirer/prompts': specifier: ^7.0.0 - version: 7.0.0(@types/node@22.7.4) + version: 7.0.0(@types/node@22.7.5) dotenv: specifier: ^16.4.5 version: 16.4.5 + fluent-ffmpeg: + specifier: ^2.1.3 + version: 2.1.3 globby: specifier: ^14.0.2 version: 14.0.2 + hash-object: + specifier: ^5.0.1 + version: 5.0.1 + hh-mm-ss: + specifier: ^1.2.0 + version: 1.2.0 kindle-api-ky: specifier: ^1.0.1 version: 1.0.1 + ky: + specifier: ^1.7.2 + version: 1.7.2 + node-id3: + specifier: ^0.2.6 + version: 0.2.6 openai-fetch: - specifier: ^3.2.0 - version: 3.2.0 + specifier: ^3.3.1 + version: 3.3.1 p-map: specifier: ^7.0.2 version: 7.0.2 @@ -35,6 +50,9 @@ importers: playwright-core: specifier: ^1.47.2 version: 1.47.2 + unrealspeech-api: + specifier: ^1.0.2 + version: 1.0.2 devDependencies: '@fisch0920/eslint-config': specifier: ^1.4.0 @@ -42,15 +60,21 @@ importers: '@total-typescript/ts-reset': specifier: ^0.6.1 version: 0.6.1 + '@types/fluent-ffmpeg': + specifier: ^2.1.26 + version: 2.1.26 + '@types/hh-mm-ss': + specifier: ^1.2.3 + version: 1.2.3 '@types/node': - specifier: ^22.4.0 - version: 22.7.4 + specifier: ^22.7.5 + version: 22.7.5 '@types/pdfkit': specifier: ^0.13.5 version: 0.13.5 del-cli: - specifier: ^5.1.0 - version: 5.1.0 + specifier: ^6.0.0 + version: 6.0.0 delay: specifier: ^6.0.0 version: 6.0.0 @@ -71,7 +95,7 @@ importers: version: 5.6.2 vitest: specifier: 2.1.2 - version: 2.1.2(@types/node@22.7.4) + version: 2.1.2(@types/node@22.7.5) packages: @@ -446,14 +470,17 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/fluent-ffmpeg@2.1.26': + resolution: {integrity: sha512-0JVF3wdQG+pN0ImwWD0bNgJiKF2OHg/7CDBHw5UIbRTvlnkgGHK6V5doE54ltvhud4o31/dEiHm23CAlxFiUQg==} + + '@types/hh-mm-ss@1.2.3': + resolution: {integrity: sha512-pq7ntXovS/jF6ayHs/CH8b27Fd52skZPf/TE+gl/oWEP6884xSmnAAySUrzaox5ebigqhzPdtcvUAu0AzG54ZA==} + '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/minimist@1.2.5': - resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} - - '@types/node@22.7.4': - resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==} + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -562,10 +589,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - aggregate-error@4.0.1: - resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==} - engines: {node: '>=12'} - ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -631,10 +654,6 @@ packages: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} - arrify@1.0.1: - resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} - engines: {node: '>=0.10.0'} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -642,6 +661,9 @@ packages: ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + async@0.2.10: + resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -698,14 +720,6 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase-keys@7.0.2: - resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} - engines: {node: '>=12'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - caniuse-lite@1.0.30001667: resolution: {integrity: sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==} @@ -736,10 +750,6 @@ packages: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} - clean-stack@4.2.0: - resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} - engines: {node: '>=12'} - cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -806,17 +816,9 @@ packages: supports-color: optional: true - decamelize-keys@1.1.1: - resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} - engines: {node: '>=0.10.0'} - - decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - - decamelize@5.0.1: - resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} - engines: {node: '>=10'} + decircular@0.1.1: + resolution: {integrity: sha512-V2Vy+QYSXdgxRPmOZKQWCDf1KQNTUP/Eqswv/3W20gz7+6GB1HTosNrWqK3PqstVpFw/Dd/cGTmXSTKPeOiGVg==} + engines: {node: '>=18'} deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} @@ -837,14 +839,14 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - del-cli@5.1.0: - resolution: {integrity: sha512-xwMeh2acluWeccsfzE7VLsG3yTr7nWikbfw+xhMnpRrF15pGSkw+3/vJZWlGoE4I86UiLRNHicmKt4tkIX9Jtg==} - engines: {node: '>=14.16'} + del-cli@6.0.0: + resolution: {integrity: sha512-9nitGV2W6KLFyya4qYt4+9AKQFL+c0Ehj5K7V7IwlxTc6RMCfQUGY9E9pLG6e8TQjtwXpuiWIGGZb3mfVxyZkw==} + engines: {node: '>=18'} hasBin: true - del@7.1.0: - resolution: {integrity: sha512-v2KyNk7efxhlyHpjEvfyxaAihKKK0nWCuf6ZtqZcFFpQRG0bJ12Qsr0RpvsICMjAAZ8DOVCxrlqpxISlMHC4Kg==} - engines: {node: '>=14.16'} + del@8.0.0: + resolution: {integrity: sha512-R6ep6JJ+eOBZsBr9esiNN1gxFbZE4Q2cULkUSFumGYecAiS6qodDvcPx/sFuWHMNul7DWmrtoEOpYSm7o6tbSA==} + engines: {node: '>=18'} delay@6.0.0: resolution: {integrity: sha512-2NJozoOHQ4NuZuVIr5CWd0iiLVIRSDepakaovIN+9eIDHEhdCAEvSy2cuf1DCrPPQLvHmbqTHODlhHg8UCy4zw==} @@ -936,10 +938,6 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - eslint-config-prettier@9.1.0: resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} hasBin: true @@ -1136,6 +1134,10 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + fluent-ffmpeg@2.1.3: + resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} + engines: {node: '>=18'} + fontkit@1.9.0: resolution: {integrity: sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==} @@ -1207,10 +1209,6 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - globby@13.2.2: - resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - globby@14.0.2: resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} engines: {node: '>=18'} @@ -1224,10 +1222,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - hard-rejection@2.1.0: - resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} - engines: {node: '>=6'} - has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -1254,21 +1248,28 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hash-object@5.0.1: + resolution: {integrity: sha512-iaRY4jYOow1caHkXW7wotYRjZDQk2nq4U7904anGJj8l4x1SLId+vuR8RpGoywZz9puD769hNFVFLFH9t+baJw==} + engines: {node: '>=18'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hh-mm-ss@1.2.0: + resolution: {integrity: sha512-f4I9Hz1dLpX/3mrEs7yq30+FiuO3tt5NWAqAGeBTaoeoBfB8vhcQ3BphuDc5DjZb/K809agqrAaFlP0jhEU/8w==} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - hosted-git-info@4.1.0: - resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} - engines: {node: '>=10'} - iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.2: + resolution: {integrity: sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1285,10 +1286,6 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - indent-string@5.0.0: - resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} - engines: {node: '>=12'} - inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -1380,6 +1377,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-obj@3.0.0: + resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} + engines: {node: '>=12'} + is-path-cwd@3.0.0: resolution: {integrity: sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1392,9 +1393,9 @@ packages: resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} engines: {node: '>=12'} - is-plain-obj@1.1.0: - resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} - engines: {node: '>=0.10.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} @@ -1486,10 +1487,6 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - kindle-api-ky@1.0.1: resolution: {integrity: sha512-ZPGtUgR97C3q/kCCLAK3RGTz0tXYUFSHOFs55mylheAO/yljH3rRHMwTxCcIeqSyhnakckokpNKZB83DpjmSeQ==} engines: {node: '>=18'} @@ -1533,28 +1530,16 @@ packages: loupe@3.1.1: resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} - map-obj@1.0.1: - resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} - engines: {node: '>=0.10.0'} - - map-obj@4.3.0: - resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} - engines: {node: '>=8'} - memorystream@0.3.1: resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} engines: {node: '>= 0.10.0'} - meow@10.1.5: - resolution: {integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} @@ -1575,10 +1560,6 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - minimist-options@4.1.0: - resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} - engines: {node: '>= 6'} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1597,16 +1578,15 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + node-id3@0.2.6: + resolution: {integrity: sha512-w8GuKXLlPpDjTxLowCt/uYMhRQzED3cg2GdSG1i6RSGKeDzPvxlXeLQuQInKljahPZ0aDnmyX7FX8BbJOM7REg==} + node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} - normalize-package-data@3.0.3: - resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} - engines: {node: '>=10'} - npm-normalize-package-bin@3.0.1: resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -1659,8 +1639,8 @@ packages: resolution: {integrity: sha512-M7CJbmv7UCopc0neRKdzfoGWaVZC+xC1925GitKH9EAqYFzX9//25Q7oX4+jw0tiCCj+t5l6VZh8UPH23NZkMA==} hasBin: true - openai-fetch@3.2.0: - resolution: {integrity: sha512-s6jcGgsuMHiOQQOdmai+F/BpNBPGttPmeEu61NY330GBJftTCeOiXbeHQCqQfF4+KJr3TwiIuZVTdhsvFfpZDA==} + openai-fetch@3.3.1: + resolution: {integrity: sha512-/b7rPeKLgS+3C2dxQHPiWDj4wOcbL/SF5L2dxktmJyfFza/VK6Mr3+rIldgGxRNpqsa3oonEowafPNx5Tdq9dA==} engines: {node: '>=18'} optionator@0.9.4: @@ -1687,10 +1667,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-map@5.5.0: - resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==} - engines: {node: '>=12'} - p-map@7.0.2: resolution: {integrity: sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q==} engines: {node: '>=18'} @@ -1803,10 +1779,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -1818,22 +1790,10 @@ packages: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} - read-pkg-up@8.0.0: - resolution: {integrity: sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==} - engines: {node: '>=12'} - read-pkg@5.2.0: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} - read-pkg@6.0.0: - resolution: {integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==} - engines: {node: '>=12'} - - redent@4.0.0: - resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} - engines: {node: '>=12'} - reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -1953,14 +1913,14 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - slash@4.0.0: - resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} - engines: {node: '>=12'} - slash@5.1.0: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} + sort-keys@5.1.0: + resolution: {integrity: sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==} + engines: {node: '>=12'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2024,10 +1984,6 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} - strip-indent@4.0.0: - resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} - engines: {node: '>=12'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -2080,10 +2036,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - trim-newlines@4.1.1: - resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==} - engines: {node: '>=12'} - ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -2116,9 +2068,9 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} - type-fest@1.4.0: - resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} - engines: {node: '>=10'} + type-fest@4.26.1: + resolution: {integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==} + engines: {node: '>=16'} typed-array-buffer@1.0.2: resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} @@ -2157,6 +2109,10 @@ packages: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} + unrealspeech-api@1.0.2: + resolution: {integrity: sha512-1dUkP+ttGoKrFkarVmVr/bFYpmNGOhK9ndvJsW/c4uYxUmUgeRD5wmrddEHlzkR+uEh/7LIjDIsRTY9HIqfdaQ==} + engines: {node: '>=18'} + update-browserslist-db@1.1.1: resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true @@ -2249,6 +2205,10 @@ packages: resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} engines: {node: '>= 0.4'} + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2270,13 +2230,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - - yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2285,6 +2238,9 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} + zero-fill@2.2.4: + resolution: {integrity: sha512-/N5GEDauLHz2uGnuJXWO1Wfib4EC+q4yp9C1jojM7RubwEKADqIqMcYpETMm1lRop403fi3v1qTOdgDE8DIOdw==} + snapshots: '@babel/code-frame@7.25.7': @@ -2450,27 +2406,27 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} - '@inquirer/checkbox@4.0.0(@types/node@22.7.4)': + '@inquirer/checkbox@4.0.0(@types/node@22.7.5)': dependencies: - '@inquirer/core': 10.0.0(@types/node@22.7.4) + '@inquirer/core': 10.0.0(@types/node@22.7.5) '@inquirer/figures': 1.0.7 - '@inquirer/type': 3.0.0(@types/node@22.7.4) + '@inquirer/type': 3.0.0(@types/node@22.7.5) ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 transitivePeerDependencies: - '@types/node' - '@inquirer/confirm@5.0.0(@types/node@22.7.4)': + '@inquirer/confirm@5.0.0(@types/node@22.7.5)': dependencies: - '@inquirer/core': 10.0.0(@types/node@22.7.4) - '@inquirer/type': 3.0.0(@types/node@22.7.4) + '@inquirer/core': 10.0.0(@types/node@22.7.5) + '@inquirer/type': 3.0.0(@types/node@22.7.5) transitivePeerDependencies: - '@types/node' - '@inquirer/core@10.0.0(@types/node@22.7.4)': + '@inquirer/core@10.0.0(@types/node@22.7.5)': dependencies: '@inquirer/figures': 1.0.7 - '@inquirer/type': 3.0.0(@types/node@22.7.4) + '@inquirer/type': 3.0.0(@types/node@22.7.5) ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 @@ -2481,91 +2437,91 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@inquirer/editor@4.0.0(@types/node@22.7.4)': + '@inquirer/editor@4.0.0(@types/node@22.7.5)': dependencies: - '@inquirer/core': 10.0.0(@types/node@22.7.4) - '@inquirer/type': 3.0.0(@types/node@22.7.4) + '@inquirer/core': 10.0.0(@types/node@22.7.5) + '@inquirer/type': 3.0.0(@types/node@22.7.5) external-editor: 3.1.0 transitivePeerDependencies: - '@types/node' - '@inquirer/expand@4.0.0(@types/node@22.7.4)': + '@inquirer/expand@4.0.0(@types/node@22.7.5)': dependencies: - '@inquirer/core': 10.0.0(@types/node@22.7.4) - '@inquirer/type': 3.0.0(@types/node@22.7.4) + '@inquirer/core': 10.0.0(@types/node@22.7.5) + '@inquirer/type': 3.0.0(@types/node@22.7.5) yoctocolors-cjs: 2.1.2 transitivePeerDependencies: - '@types/node' '@inquirer/figures@1.0.7': {} - '@inquirer/input@4.0.0(@types/node@22.7.4)': + '@inquirer/input@4.0.0(@types/node@22.7.5)': dependencies: - '@inquirer/core': 10.0.0(@types/node@22.7.4) - '@inquirer/type': 3.0.0(@types/node@22.7.4) + '@inquirer/core': 10.0.0(@types/node@22.7.5) + '@inquirer/type': 3.0.0(@types/node@22.7.5) transitivePeerDependencies: - '@types/node' - '@inquirer/number@3.0.0(@types/node@22.7.4)': + '@inquirer/number@3.0.0(@types/node@22.7.5)': dependencies: - '@inquirer/core': 10.0.0(@types/node@22.7.4) - '@inquirer/type': 3.0.0(@types/node@22.7.4) + '@inquirer/core': 10.0.0(@types/node@22.7.5) + '@inquirer/type': 3.0.0(@types/node@22.7.5) transitivePeerDependencies: - '@types/node' - '@inquirer/password@4.0.0(@types/node@22.7.4)': + '@inquirer/password@4.0.0(@types/node@22.7.5)': dependencies: - '@inquirer/core': 10.0.0(@types/node@22.7.4) - '@inquirer/type': 3.0.0(@types/node@22.7.4) + '@inquirer/core': 10.0.0(@types/node@22.7.5) + '@inquirer/type': 3.0.0(@types/node@22.7.5) ansi-escapes: 4.3.2 transitivePeerDependencies: - '@types/node' - '@inquirer/prompts@7.0.0(@types/node@22.7.4)': - dependencies: - '@inquirer/checkbox': 4.0.0(@types/node@22.7.4) - '@inquirer/confirm': 5.0.0(@types/node@22.7.4) - '@inquirer/editor': 4.0.0(@types/node@22.7.4) - '@inquirer/expand': 4.0.0(@types/node@22.7.4) - '@inquirer/input': 4.0.0(@types/node@22.7.4) - '@inquirer/number': 3.0.0(@types/node@22.7.4) - '@inquirer/password': 4.0.0(@types/node@22.7.4) - '@inquirer/rawlist': 4.0.0(@types/node@22.7.4) - '@inquirer/search': 3.0.0(@types/node@22.7.4) - '@inquirer/select': 4.0.0(@types/node@22.7.4) + '@inquirer/prompts@7.0.0(@types/node@22.7.5)': + dependencies: + '@inquirer/checkbox': 4.0.0(@types/node@22.7.5) + '@inquirer/confirm': 5.0.0(@types/node@22.7.5) + '@inquirer/editor': 4.0.0(@types/node@22.7.5) + '@inquirer/expand': 4.0.0(@types/node@22.7.5) + '@inquirer/input': 4.0.0(@types/node@22.7.5) + '@inquirer/number': 3.0.0(@types/node@22.7.5) + '@inquirer/password': 4.0.0(@types/node@22.7.5) + '@inquirer/rawlist': 4.0.0(@types/node@22.7.5) + '@inquirer/search': 3.0.0(@types/node@22.7.5) + '@inquirer/select': 4.0.0(@types/node@22.7.5) transitivePeerDependencies: - '@types/node' - '@inquirer/rawlist@4.0.0(@types/node@22.7.4)': + '@inquirer/rawlist@4.0.0(@types/node@22.7.5)': dependencies: - '@inquirer/core': 10.0.0(@types/node@22.7.4) - '@inquirer/type': 3.0.0(@types/node@22.7.4) + '@inquirer/core': 10.0.0(@types/node@22.7.5) + '@inquirer/type': 3.0.0(@types/node@22.7.5) yoctocolors-cjs: 2.1.2 transitivePeerDependencies: - '@types/node' - '@inquirer/search@3.0.0(@types/node@22.7.4)': + '@inquirer/search@3.0.0(@types/node@22.7.5)': dependencies: - '@inquirer/core': 10.0.0(@types/node@22.7.4) + '@inquirer/core': 10.0.0(@types/node@22.7.5) '@inquirer/figures': 1.0.7 - '@inquirer/type': 3.0.0(@types/node@22.7.4) + '@inquirer/type': 3.0.0(@types/node@22.7.5) yoctocolors-cjs: 2.1.2 transitivePeerDependencies: - '@types/node' - '@inquirer/select@4.0.0(@types/node@22.7.4)': + '@inquirer/select@4.0.0(@types/node@22.7.5)': dependencies: - '@inquirer/core': 10.0.0(@types/node@22.7.4) + '@inquirer/core': 10.0.0(@types/node@22.7.5) '@inquirer/figures': 1.0.7 - '@inquirer/type': 3.0.0(@types/node@22.7.4) + '@inquirer/type': 3.0.0(@types/node@22.7.5) ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 transitivePeerDependencies: - '@types/node' - '@inquirer/type@3.0.0(@types/node@22.7.4)': + '@inquirer/type@3.0.0(@types/node@22.7.5)': dependencies: - '@types/node': 22.7.4 + '@types/node': 22.7.5 '@jridgewell/sourcemap-codec@1.5.0': {} @@ -2645,11 +2601,15 @@ snapshots: '@types/estree@1.0.6': {} - '@types/json5@0.0.29': {} + '@types/fluent-ffmpeg@2.1.26': + dependencies: + '@types/node': 22.7.5 + + '@types/hh-mm-ss@1.2.3': {} - '@types/minimist@1.2.5': {} + '@types/json5@0.0.29': {} - '@types/node@22.7.4': + '@types/node@22.7.5': dependencies: undici-types: 6.19.8 @@ -2657,7 +2617,7 @@ snapshots: '@types/pdfkit@0.13.5': dependencies: - '@types/node': 22.7.4 + '@types/node': 22.7.5 '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2)': dependencies: @@ -2749,13 +2709,13 @@ snapshots: chai: 5.1.1 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.7.4))': + '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.7.5))': dependencies: '@vitest/spy': 2.1.2 estree-walker: 3.0.3 magic-string: 0.30.11 optionalDependencies: - vite: 5.4.8(@types/node@22.7.4) + vite: 5.4.8(@types/node@22.7.5) '@vitest/pretty-format@2.1.2': dependencies: @@ -2788,11 +2748,6 @@ snapshots: acorn@8.12.1: {} - aggregate-error@4.0.1: - dependencies: - clean-stack: 4.2.0 - indent-string: 5.0.0 - ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -2889,12 +2844,12 @@ snapshots: is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.3 - arrify@1.0.1: {} - assertion-error@2.0.1: {} ast-types-flow@0.0.8: {} + async@0.2.10: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.0.0 @@ -2947,15 +2902,6 @@ snapshots: callsites@3.1.0: {} - camelcase-keys@7.0.2: - dependencies: - camelcase: 6.3.0 - map-obj: 4.3.0 - quick-lru: 5.1.1 - type-fest: 1.4.0 - - camelcase@6.3.0: {} - caniuse-lite@1.0.30001667: {} chai@5.1.1: @@ -2987,10 +2933,6 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 - clean-stack@4.2.0: - dependencies: - escape-string-regexp: 5.0.0 - cli-width@4.1.0: {} clone@2.1.2: {} @@ -3049,14 +2991,7 @@ snapshots: dependencies: ms: 2.1.3 - decamelize-keys@1.1.1: - dependencies: - decamelize: 1.2.0 - map-obj: 1.0.1 - - decamelize@1.2.0: {} - - decamelize@5.0.1: {} + decircular@0.1.1: {} deep-eql@5.0.2: {} @@ -3095,21 +3030,19 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - del-cli@5.1.0: + del-cli@6.0.0: dependencies: - del: 7.1.0 - meow: 10.1.5 + del: 8.0.0 + meow: 13.2.0 - del@7.1.0: + del@8.0.0: dependencies: - globby: 13.2.2 - graceful-fs: 4.2.11 + globby: 14.0.2 is-glob: 4.0.3 is-path-cwd: 3.0.0 is-path-inside: 4.0.0 - p-map: 5.5.0 - rimraf: 3.0.2 - slash: 4.0.0 + p-map: 7.0.2 + slash: 5.1.0 delay@6.0.0: {} @@ -3280,8 +3213,6 @@ snapshots: escape-string-regexp@4.0.0: {} - escape-string-regexp@5.0.0: {} - eslint-config-prettier@9.1.0(eslint@8.57.1): dependencies: eslint: 8.57.1 @@ -3575,6 +3506,11 @@ snapshots: flatted@3.3.1: {} + fluent-ffmpeg@2.1.3: + dependencies: + async: 0.2.10 + which: 1.3.1 + fontkit@1.9.0: dependencies: '@swc/helpers': 0.3.17 @@ -3667,14 +3603,6 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - globby@13.2.2: - dependencies: - dir-glob: 3.0.1 - fast-glob: 3.3.2 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 4.0.0 - globby@14.0.2: dependencies: '@sindresorhus/merge-streams': 2.3.0 @@ -3692,8 +3620,6 @@ snapshots: graphemer@1.4.0: {} - hard-rejection@2.1.0: {} - has-bigints@1.0.2: {} has-flag@3.0.0: {} @@ -3712,17 +3638,28 @@ snapshots: dependencies: has-symbols: 1.0.3 + hash-object@5.0.1: + dependencies: + decircular: 0.1.1 + is-obj: 3.0.0 + sort-keys: 5.1.0 + type-fest: 4.26.1 + hasown@2.0.2: dependencies: function-bind: 1.1.2 + hh-mm-ss@1.2.0: + dependencies: + zero-fill: 2.2.4 + hosted-git-info@2.8.9: {} - hosted-git-info@4.1.0: + iconv-lite@0.4.24: dependencies: - lru-cache: 6.0.0 + safer-buffer: 2.1.2 - iconv-lite@0.4.24: + iconv-lite@0.6.2: dependencies: safer-buffer: 2.1.2 @@ -3737,8 +3674,6 @@ snapshots: indent-string@4.0.0: {} - indent-string@5.0.0: {} - inflight@1.0.6: dependencies: once: 1.4.0 @@ -3825,13 +3760,15 @@ snapshots: is-number@7.0.0: {} + is-obj@3.0.0: {} + is-path-cwd@3.0.0: {} is-path-inside@3.0.3: {} is-path-inside@4.0.0: {} - is-plain-obj@1.1.0: {} + is-plain-obj@4.1.0: {} is-regex@1.1.4: dependencies: @@ -3916,8 +3853,6 @@ snapshots: dependencies: json-buffer: 3.0.1 - kind-of@6.0.3: {} - kindle-api-ky@1.0.1: dependencies: ky: 1.7.2 @@ -3961,34 +3896,13 @@ snapshots: dependencies: get-func-name: 2.0.2 - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - magic-string@0.30.11: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - map-obj@1.0.1: {} - - map-obj@4.3.0: {} - memorystream@0.3.1: {} - meow@10.1.5: - dependencies: - '@types/minimist': 1.2.5 - camelcase-keys: 7.0.2 - decamelize: 5.0.1 - decamelize-keys: 1.1.1 - hard-rejection: 2.1.0 - minimist-options: 4.1.0 - normalize-package-data: 3.0.3 - read-pkg-up: 8.0.0 - redent: 4.0.0 - trim-newlines: 4.1.1 - type-fest: 1.4.0 - yargs-parser: 20.2.9 + meow@13.2.0: {} merge2@1.4.1: {} @@ -4007,12 +3921,6 @@ snapshots: dependencies: brace-expansion: 2.0.1 - minimist-options@4.1.0: - dependencies: - arrify: 1.0.1 - is-plain-obj: 1.1.0 - kind-of: 6.0.3 - minimist@1.2.8: {} ms@2.1.3: {} @@ -4023,6 +3931,10 @@ snapshots: natural-compare@1.4.0: {} + node-id3@0.2.6: + dependencies: + iconv-lite: 0.6.2 + node-releases@2.0.18: {} normalize-package-data@2.5.0: @@ -4032,13 +3944,6 @@ snapshots: semver: 5.7.2 validate-npm-package-license: 3.0.4 - normalize-package-data@3.0.3: - dependencies: - hosted-git-info: 4.1.0 - is-core-module: 2.15.1 - semver: 7.6.3 - validate-npm-package-license: 3.0.4 - npm-normalize-package-bin@3.0.1: {} npm-run-all2@6.2.3: @@ -4102,7 +4007,7 @@ snapshots: dependencies: which-pm-runs: 1.1.0 - openai-fetch@3.2.0: + openai-fetch@3.3.1: dependencies: ky: 1.7.2 @@ -4133,10 +4038,6 @@ snapshots: dependencies: p-limit: 3.1.0 - p-map@5.5.0: - dependencies: - aggregate-error: 4.0.1 - p-map@7.0.2: {} p-throttle@6.2.0: {} @@ -4220,8 +4121,6 @@ snapshots: queue-microtask@1.2.3: {} - quick-lru@5.1.1: {} - react-is@16.13.1: {} read-package-json-fast@3.0.2: @@ -4235,12 +4134,6 @@ snapshots: read-pkg: 5.2.0 type-fest: 0.8.1 - read-pkg-up@8.0.0: - dependencies: - find-up: 5.0.0 - read-pkg: 6.0.0 - type-fest: 1.4.0 - read-pkg@5.2.0: dependencies: '@types/normalize-package-data': 2.4.4 @@ -4248,18 +4141,6 @@ snapshots: parse-json: 5.2.0 type-fest: 0.6.0 - read-pkg@6.0.0: - dependencies: - '@types/normalize-package-data': 2.4.4 - normalize-package-data: 3.0.3 - parse-json: 5.2.0 - type-fest: 1.4.0 - - redent@4.0.0: - dependencies: - indent-string: 5.0.0 - strip-indent: 4.0.0 - reflect.getprototypeof@1.0.6: dependencies: call-bind: 1.0.7 @@ -4399,10 +4280,12 @@ snapshots: slash@3.0.0: {} - slash@4.0.0: {} - slash@5.1.0: {} + sort-keys@5.1.0: + dependencies: + is-plain-obj: 4.1.0 + source-map-js@1.2.1: {} spdx-correct@3.2.0: @@ -4487,10 +4370,6 @@ snapshots: dependencies: min-indent: 1.0.1 - strip-indent@4.0.0: - dependencies: - min-indent: 1.0.1 - strip-json-comments@3.1.1: {} supports-color@5.5.0: @@ -4527,8 +4406,6 @@ snapshots: dependencies: is-number: 7.0.0 - trim-newlines@4.1.1: {} - ts-api-utils@1.3.0(typescript@5.6.2): dependencies: typescript: 5.6.2 @@ -4554,7 +4431,7 @@ snapshots: type-fest@0.8.1: {} - type-fest@1.4.0: {} + type-fest@4.26.1: {} typed-array-buffer@1.0.2: dependencies: @@ -4611,6 +4488,10 @@ snapshots: unicorn-magic@0.1.0: {} + unrealspeech-api@1.0.2: + dependencies: + ky: 1.7.2 + update-browserslist-db@1.1.1(browserslist@4.24.0): dependencies: browserslist: 4.24.0 @@ -4626,12 +4507,12 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vite-node@2.1.2(@types/node@22.7.4): + vite-node@2.1.2(@types/node@22.7.5): dependencies: cac: 6.7.14 debug: 4.3.7 pathe: 1.1.2 - vite: 5.4.8(@types/node@22.7.4) + vite: 5.4.8(@types/node@22.7.5) transitivePeerDependencies: - '@types/node' - less @@ -4643,19 +4524,19 @@ snapshots: - supports-color - terser - vite@5.4.8(@types/node@22.7.4): + vite@5.4.8(@types/node@22.7.5): dependencies: esbuild: 0.21.5 postcss: 8.4.47 rollup: 4.24.0 optionalDependencies: - '@types/node': 22.7.4 + '@types/node': 22.7.5 fsevents: 2.3.3 - vitest@2.1.2(@types/node@22.7.4): + vitest@2.1.2(@types/node@22.7.5): dependencies: '@vitest/expect': 2.1.2 - '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.7.4)) + '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.7.5)) '@vitest/pretty-format': 2.1.2 '@vitest/runner': 2.1.2 '@vitest/snapshot': 2.1.2 @@ -4670,11 +4551,11 @@ snapshots: tinyexec: 0.3.0 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.4.8(@types/node@22.7.4) - vite-node: 2.1.2(@types/node@22.7.4) + vite: 5.4.8(@types/node@22.7.5) + vite-node: 2.1.2(@types/node@22.7.5) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.7.4 + '@types/node': 22.7.5 transitivePeerDependencies: - less - lightningcss @@ -4726,6 +4607,10 @@ snapshots: gopd: 1.0.1 has-tostringtag: 1.0.2 + which@1.3.1: + dependencies: + isexe: 2.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -4745,10 +4630,8 @@ snapshots: wrappy@1.0.2: {} - yallist@4.0.0: {} - - yargs-parser@20.2.9: {} - yocto-queue@0.1.0: {} yoctocolors-cjs@2.1.2: {} + + zero-fill@2.2.4: {} diff --git a/readme.md b/readme.md index 242c325..814fa20 100644 --- a/readme.md +++ b/readme.md @@ -10,6 +10,7 @@ - [Intro](#intro) - [How does it work?](#how-does-it-work) + - [Audiobook Examples 🔥](#audiobook-examples-) - [Why is this necessary?](#why-is-this-necessary) - [Usage](#usage) - [Setup Env Vars](#setup-env-vars) @@ -17,6 +18,8 @@ - [Transcribe Book Content](#transcribe-book-content) - [Export Book as PDF](#export-book-as-pdf) - [(Optional) Export Book as EPUB](#optional-export-book-as-epub) + - [(Optional) Export Book as Markdown](#optional-export-book-as-markdown) + - [(Optional) Export Book as AI-Narrated Audiobook 🔥](#optional-export-book-as-ai-narrated-audiobook-) - [Disclaimer](#disclaimer) - [Author's Notes](#authors-notes) - [Alternative Approaches](#alternative-approaches) @@ -55,7 +58,7 @@ This [example](./examples/B0819W19WD) uses the first page of the scifi book [Rev - Playwright exports a scaled down PNG screenshot of each page's rendered content, bypassing Kindle's DRM. + For each page, we use Playwright to export a scaled down PNG screenshot of the page's rendered content, bypassing Kindle's DRM. First page of Revelation Space by Alastair Reynolds @@ -63,33 +66,75 @@ This [example](./examples/B0819W19WD) uses the first page of the scifi book [Rev - Then we convert each page's screenshot into text using one of OpenAI's vLLMs (gpt-4o or gpt-4o-mini). + We then convert each page's screenshot into text using one of OpenAI's vLLMs (gpt-4o or gpt-4o-mini). -

Mantell Sector, North Nekhebet, Resurgam, Delta Pavonis system, 2551

+

Mantell Sector, North Nekhebet, Resurgam, Delta Pavonis system, 2551

+

There was a razorstorm coming in.

+

Sylveste stood on the edge of the excavation and wondered if any of his labours would survive the night. The archaeological dig was an array of deep square shafts separated by baulks of sheer-sided soil: the classical Wheeler box-grid. The shafts went down tens of metres, walled by transparent cofferdams spun from hyperdiamond. A million years of stratified geological history pressed against the sheets. But it would take only one good dustfall—one good razorstorm—to fill the shafts almost to the surface.

+

“Confirmation, sir,” said one of his team, emerging from the crouched form of the first crawler. The man’s voice was muffled behind his breather mask. “Cuvier’s just issued a severe weather advisory for the whole North

+ + + + + After doing this for each page, we now have access to the book's full contents and metadata, so we can export it in any format we want. 🎉 + + +

Here are some output previews containing only the first page of this book:

+ + + + + -

There was a razorstorm coming in.

+### Audiobook Examples 🔥 -

Sylveste stood on the edge of the excavation and wondered if any of his labours would survive the night. The archaeological dig was an array of deep square shafts separated by baulks of sheer-sided soil: the classical Wheeler box-grid. The shafts went down tens of metres, walled by transparent cofferdams spun from hyperdiamond. A million years of stratified geological history pressed against the sheets. But it would take only one good dustfall—one good razorstorm—to fill the shafts almost to the surface.

+We can even use TTS to generate custom audiobooks. -

“Confirmation, sir,” said one of his team, emerging from the crouched form of the first crawler. The man’s voice was muffled behind his breather mask. “Cuvier’s just issued a severe weather advisory for the whole North

+Here are some auto-generated examples using a few different TTS providers & voices, containing only the first page of this book as a preview: - - + + + + + + + + + +
+ OpenAI tts-1-hd "alloy" voice
(female; solid quality but more expensive) +
- After doing this for each page, we now have access to the book's full contents and metadata, so we can export it in any format we want. 🎉 + +
+ OpenAI tts-1-hd "onyx" voice
(male; solid quality but more expensive)
- Here's a preview of the PDF output containing only the first page of this book for example purposes. + +
+ Unreal Speech "Scarlett" voice
(female; medium quality but cheaper) +
+
-> [!NOTE] -> Exporting audiobooks with AI-generated voice narration is coming soon! Please star the repo if you're interested in this feature. - ### Why is this necessary? **Kindle uses a [custom AZW3 format](https://en.wikipedia.org/wiki/Kindle_File_Format) which includes heavy DRM**, making it very difficult to access the contents of ebooks that you own. It is possible to [strip the DRM using existing tools](#alternative-approaches), but it's a serious pain in the ass, is very difficult to automate, and the "best" solution is expensive and not open source. @@ -113,7 +158,7 @@ Make sure you have `node >= 18` and [pnpm](https://pnpm.io) installed. ### Setup Env Vars -Set up these environment variables in a local `.env`: +Set up these required environment variables in a local `.env`: ```sh AMAZON_EMAIL= @@ -131,16 +176,18 @@ You can find your book's [ASIN](https://en.wikipedia.org/wiki/Amazon_Standard_Id npx tsx src/extract-kindle-book.ts ``` -- **This takes a few minutes to run.** -- This logs into your [Amazon Kindle web reader](https://read.amazon.com) using headless Chrome ([Playwright](https://playwright.dev)). It can be pretty fun to watch it run, though, so feel free to tweak the script to use `headless: false` if you want to understand what it's doing or debug things. +- _(This takes a few minutes to run)_ +- This logs into your [Amazon Kindle web reader](https://read.amazon.com) using headless Chrome ([Playwright](https://playwright.dev)). It can be pretty fun to watch it run, so feel free to tweak the script to use `headless: false` to watch it do its thing. - If your account requires 2FA, the terminal will request a code from you before proceeding. - It uses a persistent browser session, so you should only have to auth once. - Once logged in, it navigates to the web reader page for a specific book (`https://read.amazon.com/?asin=${ASIN}`). - Then it changes the reader settings to use a single column and a sans-serif font. - Then it extracts the book's table of contents. - Then it goes through each page of the book's main contents and saves a PNG screenshot of the rendered content to `out/${asin}/pages/${index}-${page}.png`. +- Example: [examples/B0819W19WD/pages](./examples/B0819W19WD/pages) - Lastly, it resets the reader to the original position so your reading progress isn't affected. - It also records some JSON metadata with the TOC, book title, author, product image, etc to `out/${asin}/metadata.json`. +- Example: [examples/B0819W19WD/metadata.json](./examples/B0819W19WD/metadata.json) > [!NOTE] > I'm pretty sure Kindle's web reader uses WebGL at least in part to render the page contents, because the content pages failed to generate when running this on a VM ([Browserbase](https://www.browserbase.com)). So if you're getting blank or invalid page screenshots, that may be the reason. @@ -151,10 +198,11 @@ npx tsx src/extract-kindle-book.ts npx tsx src/transcribe-book-content.ts ``` -- **This takes a few minutes to run.** +- _(This takes a few minutes to run)_ - This takes each of the page screenshots and runs them through a vLLM (`gpt-4o` or `gpt-4o-mini`) to extract the raw text content from each page of the book. - It then stitches these text chunks together, taking into account chapter boundaries. - The result is stored as JSON to `out/${asin}/content.json`. +- Example: [examples/B0819W19WD/content.json](./examples/B0819W19WD/content.json) ### Export Book as PDF @@ -162,10 +210,11 @@ npx tsx src/transcribe-book-content.ts npx tsx src/export-book-pdf.ts ``` -- This should run almost instantly. +- _(This should run instantly)_ - It uses [PDFKit](https://github.com/foliojs/pdfkit) under the hood. - It includes a valid table of contents for easy navigation. - The result is stored to `out/${asin}/book.pdf`. +- Example: [examples/B0819W19WD/book-preview.pdf](./examples/B0819W19WD/book-preview.pdf) ### (Optional) Export Book as EPUB @@ -178,6 +227,40 @@ ebook-convert out/B0819W19WD/book.pdf out/B0819W19WD/book.epub --enable-heuristi _([ebook-convert docs](https://manual.calibre-ebook.com/generated/en/ebook-convert.html))_ +### (Optional) Export Book as Markdown + +```sh +npx tsx src/export-book-markdown.ts +``` + +- _(This should run instantly)_ +- The result is stored to `out/${asin}/book.md`. +- Example: [examples/B0819W19WD/book-preview.md](./examples/B0819W19WD/book-preview.md) + +### (Optional) Export Book as AI-Narrated Audiobook 🔥 + +```sh +npx tsx src/export-book-audio.ts +``` + +- _This takes a few minutes to run._ +- We support two TTS engines: [OpenAI TTS](https://platform.openai.com/docs/models/tts) and [Unreal Speech TTS](https://unrealspeech.com). + - To use OpenAI, set `TTS_ENGINE=openai` (the default) + - To use Unreal Speech, set `TTS_ENGINE=unrealspeech` and `UNREAL_SPEECH_API_KEY=(your-api-key)` + - OpenAI is higher quality but more expensive; Unreal Speech is medium quality and cheaper + - To set the OpenAI voice, use `OPENAI_TTS_VOICE=onyx` (defaults to `alloy`) + - To set the Unreal Speech voice, use `UNREAL_SPEECH_VOICE='Scarlett'` (defaults to `Scarlett`) + - OpenAI TTS for a full novel (~1M tokens) is approximately **$30** (1.5GB MP3 ~21 hours long) + - Unreal Speech TTS for a full novel (~1M tokens) is approximately **$2** (1.7GB MP3 ~23 hours long) + - It should be pretty easy to support other TTS providers in the future. +- The TTS will be broken up into reasonly sized chunks and stored in `mp3` files under `out/${asin}/audio//`. + - The `` directory is based on the TTS engine settings and book contents +- After generating audio for each chunk, we use `ffmpeg` to concat them together. + - You need to have `ffmpeg` installed locally for this to work + - On Mac, `brew install ffmpeg` ([or install with more options](https://stackoverflow.com/a/55108365/2353599)) +- The resulting audiobook is stored to `out/${asin}/audio//audiobook.mp3`. +- Examples: [examples/B0819W19WD/audio-previews](./examples/B0819W19WD/audio-previews) + ## Disclaimer **This project is intended purely for personal and educational use only**. It is not endorsed or supported by Amazon / Kindle. By using this project, you agree to not hold the author or contributors responsible for any consequences resulting from its usage. diff --git a/src/export-book-audio.ts b/src/export-book-audio.ts new file mode 100644 index 0000000..6021d41 --- /dev/null +++ b/src/export-book-audio.ts @@ -0,0 +1,390 @@ +import 'dotenv/config' + +import fs from 'node:fs/promises' +import path from 'node:path' + +import ffmpeg from 'fluent-ffmpeg' +import ky from 'ky' +import ID3 from 'node-id3' +import { OpenAIClient, type SpeechParams } from 'openai-fetch' +import pMap from 'p-map' +import { UnrealSpeechClient } from 'unrealspeech-api' + +import type { BookMetadata, ContentChunk } from './types' +import { + assert, + ffmpegOnProgress, + fileExists, + getEnv, + hashObject +} from './utils' + +type TTSEngine = 'openai' | 'unrealspeech' + +async function main() { + const asin = getEnv('ASIN') + assert(asin, 'ASIN is required') + + // If force mode, we'll always regenerate all of the audio files. + const force = getEnv('FORCE') === 'true' + + // In preview mode, we only export the first page of the book. + const isPreview = getEnv('AUDIOBOOK_PREVIEW') === 'true' + + const outDir = path.join('out', asin) + const audioOutDir = path.join(outDir, isPreview ? 'audio-previews' : 'audio') + await fs.mkdir(audioOutDir, { recursive: true }) + + const content = ( + JSON.parse( + await fs.readFile(path.join(outDir, 'content.json'), 'utf8') + ) as ContentChunk[] + ) + .filter((c) => !isPreview || c.page === 1) + .concat( + isPreview + ? [ + { + index: 2, + page: 2, + text: '\n\nEnd of preview', + screenshot: '' + } + ] + : [] + ) + + const metadata = JSON.parse( + await fs.readFile(path.join(outDir, 'metadata.json'), 'utf8') + ) as BookMetadata + assert(content.length, 'no book content found') + assert(metadata.meta, 'invalid book metadata: missing meta') + assert(metadata.toc?.length, 'invalid book metadata: missing toc') + + // TTS engine configuration + const ttsEngine = (getEnv('TTS_ENGINE') as TTSEngine) ?? 'openai' + assert( + ttsEngine === 'openai' || ttsEngine === 'unrealspeech', + `Invalid TTS engine "${ttsEngine}"` + ) + const openaiEngineParams: Omit = { + model: 'tts-1-hd', + voice: (getEnv('OPENAI_TTS_VOICE') as any) ?? 'alloy', + response_format: 'mp3' + } + const unrealSpeechEngineParams: Omit< + Parameters[0], + 'text' + > = { + voiceId: getEnv('UNREAL_SPEECH_VOICE') ?? 'Scarlett' + } + const ttsEngineParams: any = + ttsEngine === 'openai' ? openaiEngineParams : unrealSpeechEngineParams + const ttsEngineVoice = + ttsEngine === 'openai' + ? openaiEngineParams.voice + : unrealSpeechEngineParams.voiceId + assert(ttsEngineVoice, 'Invalid TTS engine config: missing voice') + + const unrealSpeech = + ttsEngine === 'unrealspeech' ? new UnrealSpeechClient() : undefined + const openai = ttsEngine === 'openai' ? new OpenAIClient() : undefined + const maxCharactersPerAudioBatch = ttsEngine === 'openai' ? 4096 : 3000 + + const title = metadata.meta.title + const authors = metadata.meta.authorList + + const configDirHash = hashObject({ + ttsEngine, + ttsEngineParams, + title, + authors, + content + }) + const configDir = `${ttsEngine}-${ttsEngineVoice}-${configDirHash}` + const ttsOutDir = path.join(audioOutDir, configDir) + await fs.mkdir(ttsOutDir, { recursive: true }) + + const batches: Array<{ + title?: string + text: string + }> = [] + + batches.push({ + title, + text: `Audiobook Preview of ${title} + +By ${authors.join(', ')}` + }) + + // let lastTocItemIndex = 0 + for (let i = 0, index = 0; i < metadata.toc.length - 1; i++) { + const tocItem = metadata.toc[i]! + if (tocItem.page === undefined) continue + + const nextTocItem = metadata.toc[i + 1]! + let nextIndex = nextTocItem.page + ? content.findIndex( + (c, j) => + c.page >= nextTocItem.page! || + (isPreview && j === content.length - 1) + ) + : content.length + if (nextIndex < index || (isPreview && nextIndex === index)) continue + if (isPreview) { + nextIndex = content.length + } + // lastTocItemIndex = i + + // Aggregate the text + const chunks = content.slice(index, nextIndex) + const text = chunks + .map((chunk) => chunk.text) + .join(' ') + .replaceAll('\n', '\n\n') + + // Split the text in this chapter into paragraphs. + const t = `${tocItem.title} + +${text}`.split('\n\n') + + // Combine successive paragraphs if they can fit with a single audio batch. + let j = 0 + do { + const chunk = t[j]! + + if (chunk.length > maxCharactersPerAudioBatch) { + throw new Error( + `TODO: handle large paragraphs ${chunk.length} characters: ${chunk}` + ) + } + + if (j < t.length - 1) { + const nextChunk = t[j + 1]! + + const combined = `${chunk}\n\n${nextChunk}` + if (combined.length <= maxCharactersPerAudioBatch) { + t[j] = combined + t.splice(j + 1, 1) + continue + } + } + + ++j + } while (j < t.length) + + for (const [k, element] of t.entries()) { + batches.push({ + title: k === 0 ? tocItem.title : undefined, + text: element! + }) + } + + index = nextIndex + } + + console.log() + console.log(batches) + console.log( + `\nGenerating audio for ${batches.length} batches to ${ttsOutDir}` + ) + const audioPadding = `${batches.length}`.length + + const audioChunks = await pMap( + batches, + async (batch, index) => { + const audioBaseFilename = `${index}`.padStart(audioPadding, '0') + const audioFilePath = path.join(ttsOutDir, `${audioBaseFilename}.mp3`) + const result = { ...batch, audioFilePath } + + // Don't recreate the audio file for this batch if it already exists. + // Allow `process.env.FORCE` to override this behavior. + if (!force && (await fileExists(audioFilePath))) { + console.log(`Skipping audio batch ${index + 1}: ${audioFilePath}`) + return result + } + + console.log(`Generating audio batch ${index + 1}: ${audioFilePath}`) + let audio: ArrayBuffer + + if (ttsEngine === 'openai') { + audio = await openai!.createSpeech({ + ...ttsEngineParams, + input: batch.text + }) + } else { + const res = await unrealSpeech!.speech({ + ...ttsEngineParams, + text: batch.text + }) + console.log(res) + + audio = await ky.get(res.OutputUri).arrayBuffer() + } + + await fs.writeFile(audioFilePath, Buffer.from(audio)) + return result + }, + { concurrency: 32 } + ) + + const audioParts = await pMap( + audioChunks, + async (audioChunk) => { + const probeData = await ffmpegProbe(audioChunk.audioFilePath) + + const duration = + probeData.format.duration ?? + (probeData.streams[0]?.duration as unknown as number) + assert( + duration !== undefined && !Number.isNaN(duration), + `Failed to determine audio duration for file: ${audioChunk.audioFilePath}` + ) + + return { + ...audioChunk, + duration + } + }, + { concurrency: 32 } + ) + + const audioConcatInputFilePath = path.join(ttsOutDir, 'files.txt') + const audioConcatInput = audioParts + .map((a) => `file ${path.basename(a.audioFilePath)}`) + .join('\n') + await fs.writeFile(audioConcatInputFilePath, audioConcatInput) + const audiobookOutputFilePath = path.join(ttsOutDir, 'audiobook.mp3') + + const expectedDurationMs = + audioParts.reduce((duration, a) => duration + a.duration, 0) * 1000 + + console.log( + `\nUsing ffmpeg to concat audiobook from ${audioParts.length} files...` + ) + + // Use ffmpeg to concatenate the audio files into a single audiobook file. + await new Promise((resolve, reject) => { + ffmpeg(audioConcatInputFilePath) + .inputOptions(['-f', 'concat']) + .withOptions([ + // metadata (mp3 tags) + '-metadata', + `title="${isPreview ? 'Preview of ' : ''}${title}"` + // TODO: fluent-ffmpeg is choking on this metadata tag for some reason + // '-metadata', + // `artist="${authors.join('/')}"`, + // '-metadata', + // `encoded_by="https://github.com/transitive-bullshit/kindle-ai-export"` + ]) + .outputOptions([ + // misc + '-hide_banner', + '-map_metadata', + '-1', + '-map_chapters', + '-1', + + // audio + '-c', + 'copy' + ]) + .output(audiobookOutputFilePath) + .on('start', (cmd) => console.log({ cmd })) + .on( + 'progress', + ffmpegOnProgress((progress) => { + console.log(`Processing audio: ${Math.floor(progress * 100)}%`) + }, expectedDurationMs) + ) + .on('end', () => resolve()) + .on('error', (err) => reject(err)) + .run() + }) + + try { + // Add ID3 metadata to the MP3 audiobook file. + await new Promise((resolve, reject) => { + const res = ID3.update( + { + title: isPreview ? `Preview of ${title}` : title, + artist: authors.join('/'), + encodedBy: 'https://github.com/transitive-bullshit/kindle-ai-export', + commercialUrl: [`https://www.amazon.com/dp/${asin}`] + // image: 'https://m.media-amazon.com/images/I/41sMaof0iQL.jpg', // TODO + // TODO: these tags don't seem to be working properly in the node-id3 library + // chapter: metadata.toc + // .map((tocItem, index) => { + // if (tocItem.page === undefined) return undefined + // if (index > lastTocItemIndex) return undefined + + // const nextTocItem = metadata.toc[index + 1] + // const audioPartIndexTocItem = audioParts.findIndex( + // (a) => a.title === tocItem.title + // ) + // const audioPartIndexNextTocItem = nextTocItem + // ? audioParts.findIndex((a) => a.title === nextTocItem.title) + // : audioParts.length + // const startTimeMs = Math.floor( + // 1000 * + // audioParts + // .slice(0, audioPartIndexTocItem) + // .reduce((duration, a) => duration + a.duration, 0) + // ) + // const endTimeMs = Math.ceil( + // 1000 * + // audioParts + // .slice(0, audioPartIndexNextTocItem) + // .reduce((duration, a) => duration + a.duration, 0) + // ) + // console.log(index, tocItem.title, { startTimeMs, endTimeMs }) + // return { + // elementID: tocItem.title, + // startTimeMs, + // endTimeMs + // } + // }) + // .filter(Boolean), + // tableOfContents: [ + // { + // elementID: 'TOC', + // isOrdered: true, + // elements: metadata.toc + // .map((tocItem, index) => { + // if (tocItem.page === undefined) return undefined + // if (index > lastTocItemIndex) return undefined + + // return tocItem.title + // }) + // .filter(Boolean) + // } + // ] + }, + audiobookOutputFilePath + ) + + if (res !== true) { + reject(res) + } else { + resolve() + } + }) + } catch (err: any) { + console.warn( + `(warning) Failed to add extra ID3 metadata to audiobook: ${err.message}\n` + ) + } + + console.log(`\nGenerated audiobook: ${audiobookOutputFilePath}`) +} + +async function ffmpegProbe(filePath: string) { + return new Promise((resolve, reject) => { + ffmpeg.ffprobe(filePath, (err, data) => { + if (err) return reject(err) + resolve(data) + }) + }) +} + +await main() diff --git a/src/export-book-markdown.ts b/src/export-book-markdown.ts new file mode 100644 index 0000000..2fabbd7 --- /dev/null +++ b/src/export-book-markdown.ts @@ -0,0 +1,92 @@ +import 'dotenv/config' + +import fs from 'node:fs/promises' +import path from 'node:path' + +import type { BookMetadata, ContentChunk } from './types' +import { assert, getEnv } from './utils' + +async function main() { + const asin = getEnv('ASIN') + assert(asin, 'ASIN is required') + + const outDir = path.join('out', asin) + + const content = JSON.parse( + await fs.readFile(path.join(outDir, 'content.json'), 'utf8') + ) as ContentChunk[] + const metadata = JSON.parse( + await fs.readFile(path.join(outDir, 'metadata.json'), 'utf8') + ) as BookMetadata + assert(content.length, 'no book content found') + assert(metadata.meta, 'invalid book metadata: missing meta') + assert(metadata.toc?.length, 'invalid book metadata: missing toc') + + const title = metadata.meta.title + const authors = metadata.meta.authorList + + let lastTocItemIndex = 0 + for (let i = 0, index = 0; i < metadata.toc.length - 1; i++) { + const tocItem = metadata.toc[i]! + if (tocItem.page === undefined) continue + + const nextTocItem = metadata.toc[i + 1]! + const nextIndex = nextTocItem.page + ? content.findIndex((c) => c.page >= nextTocItem.page!) + : content.length + if (nextIndex < index) continue + + lastTocItemIndex = i + } + + let output = `# ${title} + +By ${authors.join(', ')} + +--- + +## Table of Contents + +${metadata.toc + .filter( + (tocItem, index) => tocItem.page !== undefined && index <= lastTocItemIndex + ) + .map( + (tocItem) => + `- [${tocItem.title}](#${tocItem.title.toLowerCase().replaceAll(/[^\da-z]+/g, '-')})` + ) + .join('\n')} + +---` + + for (let i = 0, index = 0; i < metadata.toc.length - 1; i++) { + const tocItem = metadata.toc[i]! + if (tocItem.page === undefined) continue + + const nextTocItem = metadata.toc[i + 1]! + const nextIndex = nextTocItem.page + ? content.findIndex((c) => c.page >= nextTocItem.page!) + : content.length + if (nextIndex < index) continue + + const chunks = content.slice(index, nextIndex) + + const text = chunks + .map((chunk) => chunk.text) + .join(' ') + .replaceAll('\n', '\n\n') + + output += ` + +## ${tocItem.title} + +${text}` + + index = nextIndex + } + + await fs.writeFile(path.join(outDir, 'book.md'), output) + console.log(output) +} + +await main() diff --git a/src/export-book-pdf.ts b/src/export-book-pdf.ts index eb36d41..a620617 100644 --- a/src/export-book-pdf.ts +++ b/src/export-book-pdf.ts @@ -1,4 +1,3 @@ -#!/usr/bin/env node import 'dotenv/config' import fs from 'node:fs' @@ -79,14 +78,10 @@ async function main() { if (needsNewPage) { doc.addPage() } - // const chunks = content.slice(index, Math.min(nextIndex, index + 2)) // for preview - const chunks = content.slice(index, nextIndex) - const text = chunks - .map((chunk) => chunk.text) - .join(' ') - .replaceAll(/\n+/g, '\n') - .replaceAll(/^\s*/gm, '') + // Aggregate all of the chunks in this chapter into a single string. + const chunks = content.slice(index, nextIndex) + const text = chunks.map((chunk) => chunk.text).join(' ') ;(doc as any).outline.addItem(tocItem.title) doc.fontSize(20) @@ -103,14 +98,8 @@ async function main() { index = nextIndex needsNewPage = true - // break } - // for preview - // doc.addPage() - // doc.fontSize(20) - // doc.text('(End of Preview)', { align: 'center', lineGap: 16 }) - doc.end() await new Promise((resolve, reject) => { stream.on('finish', resolve) @@ -118,9 +107,4 @@ async function main() { }) } -try { - await main() -} catch (err) { - console.error('error', err) - process.exit(1) -} +await main() diff --git a/src/utils.ts b/src/utils.ts index 00ac337..d2d2e18 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,8 @@ +import fs from 'node:fs/promises' + +import hashObjectImpl from 'hash-object' +import timeFormat from 'hh-mm-ss' + export { assert, getEnv, @@ -19,3 +24,58 @@ export function deromanize(romanNumeral: string): number { return num } + +export async function fileExists( + filePath: string, + mode: number = fs.constants.F_OK | fs.constants.R_OK +): Promise { + try { + await fs.access(filePath, mode) + return true + } catch { + return false + } +} + +export function hashObject(obj: Record): string { + return hashObjectImpl(obj, { + algorithm: 'sha1', + encoding: 'hex' + }) +} + +export type FfmpegProgressEvent = { + frames: number + currentFps: number + currentKbps: number + targetSize: number + timemark: string + percent?: number | undefined +} + +export function ffmpegOnProgress( + onProgress: (progress: number, event: FfmpegProgressEvent) => void, + durationMs: number +) { + return (event: FfmpegProgressEvent) => { + let progress = 0 + + try { + const timestamp = timeFormat.toMs(event.timemark) + progress = timestamp / durationMs + } catch {} + + if ( + Number.isNaN(progress) && + event.percent !== undefined && + !Number.isNaN(event.percent) + ) { + progress = event.percent / 100 + } + + if (!Number.isNaN(progress)) { + progress = Math.max(0, Math.min(1, progress)) + onProgress(progress, event) + } + } +} diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..aac0333 --- /dev/null +++ b/todo.md @@ -0,0 +1 @@ +- add github repo link to exports