diff --git a/package-lock.json b/package-lock.json index cdb6d4f..347f782 100755 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@aws-sdk/client-polly": "^3.1009.0", "@aws-sdk/client-s3": "^3.835.0", "@aws-sdk/s3-request-presigner": "^3.835.0", "@nestjs/common": "^11.1.3", @@ -22,6 +23,8 @@ "@nestjs/throttler": "^6.4.0", "@nestjs/typeorm": "^11.0.0", "@sendgrid/mail": "^8.1.5", + "@types/passport-google-oauth20": "^2.0.17", + "apple-signin-auth": "^2.0.0", "axios": "^1.10.0", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", @@ -29,7 +32,9 @@ "joi": "^17.13.3", "multer": "^2.0.1", "multer-s3": "^3.0.1", + "openai": "^6.27.0", "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pg": "^8.16.2", @@ -434,6 +439,478 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/client-polly": { + "version": "3.1009.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-polly/-/client-polly-3.1009.0.tgz", + "integrity": "sha512-FxfjTcOXKT0wF9PzHmcnFdlD+4RCYTfJrSiAko8U77WZzYN5CT4MzdFV7+uiJljaGQqOtR+0RktTBLp12h+89Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-stream": "^4.5.19", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/core": { + "version": "3.973.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.20.tgz", + "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/xml-builder": "^3.972.11", + "@smithy/core": "^3.23.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.18.tgz", + "integrity": "sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.20.tgz", + "integrity": "sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.19", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.20.tgz", + "integrity": "sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-login": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.21.tgz", + "integrity": "sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-ini": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.18.tgz", + "integrity": "sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.20.tgz", + "integrity": "sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/token-providers": "3.1009.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.20.tgz", + "integrity": "sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", + "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", + "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@smithy/core": "^3.23.11", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-retry": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/nested-clients": { + "version": "3.996.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", + "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", + "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/token-providers": { + "version": "3.1009.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1009.0.tgz", + "integrity": "sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/types": { + "version": "3.973.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", + "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", + "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.11.tgz", + "integrity": "sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "fast-xml-parser": "5.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/fast-xml-parser": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/strnum": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/@aws-sdk/client-s3": { "version": "3.835.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.835.0.tgz", @@ -658,6 +1135,289 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.20.tgz", + "integrity": "sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/core": { + "version": "3.973.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.20.tgz", + "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/xml-builder": "^3.972.11", + "@smithy/core": "^3.23.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", + "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", + "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@smithy/core": "^3.23.11", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-retry": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/nested-clients": { + "version": "3.996.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", + "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", + "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/types": { + "version": "3.973.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", + "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", + "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.11.tgz", + "integrity": "sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "fast-xml-parser": "5.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/fast-xml-parser": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/strnum": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/@aws-sdk/credential-provider-node": { "version": "3.835.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.835.0.tgz", @@ -1173,6 +1933,15 @@ "node": ">=18.0.0" } }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -4425,12 +5194,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", - "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", + "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4463,15 +5232,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.4.tgz", - "integrity": "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==", + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.11.tgz", + "integrity": "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.3", - "@smithy/types": "^4.3.1", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -4479,19 +5249,20 @@ } }, "node_modules/@smithy/core": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.5.3.tgz", - "integrity": "sha512-xa5byV9fEguZNofCclv6v9ra0FYh5FATQW/da7FQUVTic94DfrN/NvmKZjrMyzbpqfot9ZjBaO8U1UeTbmSLuA==", + "version": "3.23.11", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.11.tgz", + "integrity": "sha512-952rGf7hBRnhUIaeLp6q4MptKW8sPFe5VvkoZ5qIzFAtx6c/QZ/54FS3yootsyUSf9gJX/NBqEBNdNR7jMIlpQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.0.8", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-stream": "^4.2.2", - "@smithy/util-utf8": "^4.0.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.19", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, "engines": { @@ -4499,15 +5270,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.6.tgz", - "integrity": "sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", + "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.3", - "@smithy/property-provider": "^4.0.4", - "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -4585,15 +5356,15 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.4.tgz", - "integrity": "sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw==", + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", + "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.2", - "@smithy/querystring-builder": "^4.0.4", - "@smithy/types": "^4.3.1", - "@smithy/util-base64": "^4.0.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -4616,14 +5387,14 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz", - "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", + "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/types": "^4.13.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4645,12 +5416,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz", - "integrity": "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", + "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4658,9 +5429,9 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", - "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4684,13 +5455,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.4.tgz", - "integrity": "sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", + "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4698,18 +5469,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.12.tgz", - "integrity": "sha512-Piy/9UOjh5FtEXhybjPwyOHcC/pGHFknl2Gc/q1YbEkngxY6eQwvBvZTNamXpyDAHCuP3h+lymcVcdyO3WdGqQ==", + "version": "4.4.25", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.25.tgz", + "integrity": "sha512-dqjLwZs2eBxIUG6Qtw8/YZ4DvzHGIf0DA18wrgtfP6a50UIO7e2nY0FPdcbv5tVJKqWCCU5BmGMOUwT7Puan+A==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.5.3", - "@smithy/middleware-serde": "^4.0.8", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", - "@smithy/util-middleware": "^4.0.4", + "@smithy/core": "^3.23.11", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -4717,46 +5488,34 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.13.tgz", - "integrity": "sha512-5ILvPCJevTcGpl7wAvSV9HKbIGS2Wxz505d0b5dP9kmjBhsFm1SAsSLIteMn925hlxPUkOsjcjMyaEiQDr9s4w==", + "version": "4.4.42", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.42.tgz", + "integrity": "sha512-vbwyqHRIpIZutNXZpLAozakzamcINaRCpEy1MYmK6xBeW3xN+TyPRA123GjXnuxZIjc9848MRRCugVMTXxC4Eg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.3", - "@smithy/protocol-http": "^5.1.2", - "@smithy/service-error-classification": "^4.0.6", - "@smithy/smithy-client": "^4.4.4", - "@smithy/types": "^4.3.1", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-retry": "^4.0.6", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/middleware-retry/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@smithy/middleware-serde": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz", - "integrity": "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.14.tgz", + "integrity": "sha512-+CcaLoLa5apzSRtloOyG7lQvkUw2ZDml3hRh4QiG9WyEPfW5Ke/3tPOPiPjUneuT59Tpn8+c3RVaUvvkkwqZwg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@smithy/core": "^3.23.11", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4764,12 +5523,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz", - "integrity": "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", + "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4777,14 +5536,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.3.tgz", - "integrity": "sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==", + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", + "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.0.4", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4792,15 +5551,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", - "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "version": "4.4.16", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.16.tgz", + "integrity": "sha512-ULC8UCS/HivdCB3jhi+kLFYe4B5gxH2gi9vHBfEIiRrT2jfKiZNiETJSlzRtE6B26XbBHjPtc8iZKSNqMol9bw==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.0.4", - "@smithy/protocol-http": "^5.1.2", - "@smithy/querystring-builder": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/abort-controller": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4808,12 +5567,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.4.tgz", - "integrity": "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", + "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4821,12 +5580,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz", - "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==", + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", + "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4834,13 +5593,13 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz", - "integrity": "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", + "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", - "@smithy/util-uri-escape": "^4.0.0", + "@smithy/types": "^4.13.1", + "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4848,12 +5607,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", - "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", + "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4861,24 +5620,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.6.tgz", - "integrity": "sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", + "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1" + "@smithy/types": "^4.13.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.4.tgz", - "integrity": "sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==", + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", + "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4886,18 +5645,18 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.2.tgz", - "integrity": "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==", + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", + "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4905,17 +5664,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.4.tgz", - "integrity": "sha512-38Ivn1VoArWi+wvJeW6rGl9lcuViYjmGfaZaBgOlFEyoQSIl2Rnr3uOWzwu3FE8NIvHflQVkwbveMQxBAEbd1A==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.5.tgz", + "integrity": "sha512-UqwYawyqSr/aog8mnLnfbPurS0gi4G7IYDcD28cUIBhsvWs1+rQcL2IwkUQ+QZ7dibaoRzhNF99fAQ9AUcO00w==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.5.3", - "@smithy/middleware-endpoint": "^4.1.12", - "@smithy/middleware-stack": "^4.0.4", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", - "@smithy/util-stream": "^4.2.2", + "@smithy/core": "^3.23.11", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.19", "tslib": "^2.6.2" }, "engines": { @@ -4923,9 +5682,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", - "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", + "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4935,13 +5694,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", - "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", + "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/querystring-parser": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -4949,13 +5708,13 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", - "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4963,9 +5722,9 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", - "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4975,9 +5734,9 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", - "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4987,12 +5746,12 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", - "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5000,9 +5759,9 @@ } }, "node_modules/@smithy/util-config-provider": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", - "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -5012,15 +5771,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.20", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.20.tgz", - "integrity": "sha512-496BbDMx/8kQrvlhT0EsX7JM7yVpK7CACmG3LsqMX9RaJnF7M/OVlfbxoRceUp5o5S0HqBnV8/xGOX7MYCv2Gw==", + "version": "4.3.41", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.41.tgz", + "integrity": "sha512-M1w1Ux0rSVvBOxIIiqbxvZvhnjQ+VUjJrugtORE90BbadSTH+jsQL279KRL3Hv0w69rE7EuYkV/4Lepz/NBW9g==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.0.4", - "@smithy/smithy-client": "^4.4.4", - "@smithy/types": "^4.3.1", - "bowser": "^2.11.0", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -5028,17 +5786,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.20", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.20.tgz", - "integrity": "sha512-QsGHToYvRCoMyJQr/bXLG7L+nXNxICpG5LI1lRL0wkdkvLIxP89r4O+LHLWI9UeLzylxJ7VPnsTR/ADJ+F71/w==", + "version": "4.2.44", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.44.tgz", + "integrity": "sha512-YPze3/lD1KmWuZsl9JlfhcgGLX7AXhSoaCDtiPntUjNW5/YY0lOHjkcgxyE9x/h5vvS1fzDifMGjzqnNlNiqOQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.1.4", - "@smithy/credential-provider-imds": "^4.0.6", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/property-provider": "^4.0.4", - "@smithy/smithy-client": "^4.4.4", - "@smithy/types": "^4.3.1", + "@smithy/config-resolver": "^4.4.11", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -5046,13 +5804,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz", - "integrity": "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", + "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.3", - "@smithy/types": "^4.3.1", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -5060,9 +5818,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", - "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -5072,12 +5830,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.4.tgz", - "integrity": "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", + "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -5085,13 +5843,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.6.tgz", - "integrity": "sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", + "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.0.6", - "@smithy/types": "^4.3.1", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -5099,18 +5857,18 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.2.tgz", - "integrity": "sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w==", + "version": "4.5.19", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.19.tgz", + "integrity": "sha512-v4sa+3xTweL1CLO2UP0p7tvIMH/Rq1X4KKOxd568mpe6LSLMQCnDHs4uv7m3ukpl3HvcN2JH6jiCS0SNRXKP/w==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/node-http-handler": "^4.0.6", - "@smithy/types": "^4.3.1", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5118,9 +5876,9 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", - "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -5130,12 +5888,12 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5156,6 +5914,18 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@sqltools/formatter": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", @@ -5583,7 +6353,6 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -5594,7 +6363,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -5640,7 +6408,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -5652,7 +6419,6 @@ "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -5682,7 +6448,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { @@ -5756,7 +6521,6 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, "license": "MIT" }, "node_modules/@types/multer": { @@ -5778,16 +6542,35 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/oauth": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz", + "integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/passport": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", - "dev": true, "license": "MIT", "dependencies": { "@types/express": "*" } }, + "node_modules/@types/passport-google-oauth20": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.17.tgz", + "integrity": "sha512-MHNOd2l7gOTCn3iS+wInPQMiukliAUvMpODO3VlXxOiwNEMSyzV7UNvAdqxSN872o8OXx1SqPDVT6tLW74AtqQ==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-oauth2": "*" + } + }, "node_modules/@types/passport-jwt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", @@ -5811,6 +6594,17 @@ "@types/passport-strategy": "*" } }, + "node_modules/@types/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-6//z+4orIOy/g3zx17HyQ71GSRK4bs7Sb+zFasRoc2xzlv7ZCJ+vkDBYFci8U6HY+or6Zy7ajf4mz4rK7nsWJQ==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*" + } + }, "node_modules/@types/passport-strategy": { "version": "0.2.38", "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", @@ -5826,21 +6620,18 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -5851,7 +6642,6 @@ "version": "1.15.8", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -7134,6 +7924,19 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/apple-signin-auth": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/apple-signin-auth/-/apple-signin-auth-2.0.0.tgz", + "integrity": "sha512-/O5gvAby7OU2K7baYQCzY0e0tCiHzhFkzt9L2v3bMd2I2w0ckCZ/4hdVUOrbolRGMppME+Zo8TAKPVru8aYnzg==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "node-rsa": "^1.1.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/arch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/arch/-/arch-3.0.0.tgz", @@ -7182,6 +7985,15 @@ "dev": true, "license": "MIT" }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -7373,6 +8185,15 @@ ], "license": "MIT" }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", @@ -9094,6 +9915,21 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.3.tgz", + "integrity": "sha512-1o60KoFw2+LWKQu3IdcfcFlGTW4dpqEWmjhYec6H82AYZU2TVBXep6tMl8Z1Y+wM+ZrzCwe3BZ9Vyd9N2rIvmg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, "node_modules/fast-xml-parser": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", @@ -11686,6 +12522,15 @@ "dev": true, "license": "MIT" }, + "node_modules/node-rsa": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", + "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==", + "license": "MIT", + "dependencies": { + "asn1": "^0.2.4" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -11722,6 +12567,12 @@ "node": ">=8" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -11780,6 +12631,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.27.0.tgz", + "integrity": "sha512-osTKySlrdYrLYTt0zjhY8yp0JUBmWDCN+Q+QxsV4xMQnnoVFpylgKGgxwN8sSdTNw0G4y+WUXs4eCMWpyDNWZQ==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -11972,6 +12844,18 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/passport-jwt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", @@ -11993,6 +12877,26 @@ "node": ">= 0.4.0" } }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -12011,6 +12915,21 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", + "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -14485,6 +15404,12 @@ "node": ">=8" } }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/uint8array-extras": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", diff --git a/package.json b/package.json index cca2858..bd79801 100755 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@aws-sdk/client-polly": "^3.1009.0", "@aws-sdk/client-s3": "^3.835.0", "@aws-sdk/s3-request-presigner": "^3.835.0", "@nestjs/common": "^11.1.3", @@ -33,6 +34,8 @@ "@nestjs/throttler": "^6.4.0", "@nestjs/typeorm": "^11.0.0", "@sendgrid/mail": "^8.1.5", + "@types/passport-google-oauth20": "^2.0.17", + "apple-signin-auth": "^2.0.0", "axios": "^1.10.0", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", @@ -40,7 +43,9 @@ "joi": "^17.13.3", "multer": "^2.0.1", "multer-s3": "^3.0.1", + "openai": "^6.27.0", "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pg": "^8.16.2", diff --git a/scripts/fetch-images-wikimedia.js b/scripts/fetch-images-wikimedia.js new file mode 100755 index 0000000..54bea71 --- /dev/null +++ b/scripts/fetch-images-wikimedia.js @@ -0,0 +1,175 @@ +#!/usr/bin/env node +/** + * Script para obtener imágenes de Wikimedia Commons para monumentos + * Usa la API de Wikimedia Commons (gratuita, sin API key) + */ + +const { Client } = require('pg'); +const https = require('https'); + +// Configuración +const config = { + db: { + host: 'localhost', + port: 5432, + user: 'karibeo', + password: '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', + database: 'karibeo_db', + }, +}; + +// Buscar imágenes en Wikimedia Commons +async function searchWikimediaImages(query, limit = 3) { + return new Promise((resolve, reject) => { + // Primero buscar en Wikipedia para obtener la página del lugar + const searchUrl = `https://commons.wikimedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&srnamespace=6&srlimit=${limit}&format=json`; + + https.get(searchUrl, { headers: { 'User-Agent': 'KaribeoAI/1.0 (contact@karibeo.ai)' } }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + const json = JSON.parse(data); + if (json.query && json.query.search && json.query.search.length > 0) { + // Obtener URLs de las imágenes + const titles = json.query.search.map(s => s.title).join('|'); + getImageUrls(titles).then(resolve).catch(reject); + } else { + resolve([]); + } + } catch (e) { + reject(e); + } + }); + }).on('error', reject); + }); +} + +// Obtener URLs directas de las imágenes +async function getImageUrls(titles) { + return new Promise((resolve, reject) => { + const url = `https://commons.wikimedia.org/w/api.php?action=query&titles=${encodeURIComponent(titles)}&prop=imageinfo&iiprop=url|size&format=json`; + + https.get(url, { headers: { 'User-Agent': 'KaribeoAI/1.0 (contact@karibeo.ai)' } }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + const json = JSON.parse(data); + const urls = []; + if (json.query && json.query.pages) { + for (const pageId in json.query.pages) { + const page = json.query.pages[pageId]; + if (page.imageinfo && page.imageinfo[0]) { + const info = page.imageinfo[0]; + // Solo incluir imágenes de tamaño razonable (> 100KB y < 10MB) + if (info.size > 100000 && info.size < 10000000) { + urls.push(info.url); + } + } + } + } + resolve(urls); + } catch (e) { + reject(e); + } + }); + }).on('error', reject); + }); +} + +// Buscar con términos alternativos +async function searchWithAlternatives(name, country) { + const countryName = country === 'DO' ? 'Dominican Republic' : 'Puerto Rico'; + const countryNameEs = country === 'DO' ? 'República Dominicana' : 'Puerto Rico'; + + // Lista de búsquedas a intentar + const searches = [ + `${name} ${countryNameEs}`, + `${name} ${countryName}`, + name, + `${name} Caribbean`, + ]; + + for (const query of searches) { + console.log(` Buscando: "${query}"`); + const images = await searchWikimediaImages(query, 5); + if (images.length > 0) { + return images.slice(0, 3); // Máximo 3 imágenes + } + await delay(500); // Rate limit + } + + return []; +} + +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function main() { + console.log('🖼️ Buscador de Imágenes - Wikimedia Commons'); + console.log('============================================\n'); + + // Conectar a la DB + const client = new Client(config.db); + await client.connect(); + console.log('✅ Conectado a la base de datos\n'); + + // Obtener monumentos sin imágenes válidas + const query = ` + SELECT id, name, country, slug, images + FROM tourism.places_of_interest + WHERE active = true + AND (images IS NULL OR images::text = '[]' OR images::text LIKE '%example.com%') + ORDER BY name + `; + + const result = await client.query(query); + const places = result.rows; + console.log(`📍 Encontrados ${places.length} lugares sin imágenes válidas\n`); + + let found = 0; + let notFound = 0; + + for (let i = 0; i < places.length; i++) { + const place = places[i]; + console.log(`[${i + 1}/${places.length}] ${place.name} (${place.country})`); + + try { + const images = await searchWithAlternatives(place.name, place.country); + + if (images.length > 0) { + // Guardar en la DB + await client.query( + `UPDATE tourism.places_of_interest SET images = $1 WHERE id = $2`, + [JSON.stringify(images), place.id] + ); + console.log(` ✅ ${images.length} imágenes encontradas`); + found++; + } else { + console.log(` ⚠️ No se encontraron imágenes`); + notFound++; + } + } catch (error) { + console.log(` ❌ Error: ${error.message}`); + notFound++; + } + + // Rate limit para la API de Wikimedia + await delay(1000); + } + + await client.end(); + + console.log('\n============================================'); + console.log('📊 RESUMEN'); + console.log(` ✅ Con imágenes: ${found}`); + console.log(` ⚠️ Sin imágenes: ${notFound}`); + console.log('============================================\n'); +} + +main().catch(err => { + console.error('Error fatal:', err); + process.exit(1); +}); diff --git a/scripts/generate-audios.js b/scripts/generate-audios.js new file mode 100755 index 0000000..4fb071e --- /dev/null +++ b/scripts/generate-audios.js @@ -0,0 +1,213 @@ +#!/usr/bin/env node +/** + * Script standalone para generar audios TTS para todos los monumentos + * Usa Piper TTS directamente sin pasar por el API + */ + +const { Client } = require('pg'); +const { exec } = require('child_process'); +const { promisify } = require('util'); +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +const execAsync = promisify(exec); + +// Configuración +const config = { + db: { + host: 'localhost', + port: 5432, + user: 'karibeo', + password: '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', + database: 'karibeo_db', + }, + tts: { + piperPath: '/home/karibeo-api/karibeo-api/tts/piper/piper', + voicesPath: '/home/karibeo-api/karibeo-api/tts/voices', + cachePath: '/home/karibeo-api/karibeo-api/tts/cache', + baseUrl: 'https://api.karibeo.ai:8443', + }, + voiceMap: { + es: 'es_ES-davefx-medium', + en: 'en_US-amy-medium', + fr: 'fr_FR-siwis-medium', + it: 'it_IT-riccardo-x_low', + de: 'de_DE-thorsten-medium', + }, + languages: ['es', 'en', 'fr', 'it', 'de'], +}; + +// Limpiar texto para TTS +function cleanTextForSpeech(text) { + return text + .replace(/[\u4E00-\u9FFF]/g, '') + .replace(/[\u3400-\u4DBF]/g, '') + .replace(/[\u3040-\u309F]/g, '') + .replace(/[\u30A0-\u30FF]/g, '') + .replace(/[\uAC00-\uD7AF]/g, '') + .replace(/[\u{1F300}-\u{1F9FF}]/gu, '') + .replace(/[\u{2600}-\u{26FF}]/gu, '') + .replace(/[\u{2700}-\u{27BF}]/gu, '') + .replace(/[\u{1F600}-\u{1F64F}]/gu, '') + .replace(/[\u{1F680}-\u{1F6FF}]/gu, '') + .replace(/[\u{1F1E0}-\u{1F1FF}]/gu, '') + .replace(/[★☆✓✗✔✘●○◆◇▪▫►◄→←↑↓⇒⇐⇑⇓♠♣♥♦]/g, '') + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/\*([^*]+)\*/g, '$1') + .replace(/__([^_]+)__/g, '$1') + .replace(/_([^_]+)_/g, '$1') + .replace(/\s+/g, ' ') + .trim(); +} + +function getAudioHash(text, language) { + const content = `${language}:${text}`; + return crypto.createHash('md5').update(content).digest('hex'); +} + +async function generateAudio(text, language) { + const voice = config.voiceMap[language] || config.voiceMap['en']; + const voiceModel = path.join(config.tts.voicesPath, `${voice}.onnx`); + + if (!fs.existsSync(voiceModel)) { + console.log(` ⚠️ Modelo de voz no encontrado: ${voiceModel}`); + return null; + } + + const cleanText = cleanTextForSpeech(text) + .replace(/[\n\r]/g, ' ') + .replace(/"/g, "'") + .substring(0, 3000); + + const hash = getAudioHash(cleanText, language); + const fileName = `${hash}.wav`; + const filePath = path.join(config.tts.cachePath, fileName); + + // Si ya existe, retornar URL + if (fs.existsSync(filePath)) { + console.log(` 📦 Cache hit: ${fileName}`); + return `${config.tts.baseUrl}/api/v1/tts/audio/${fileName}`; + } + + // Crear directorio si no existe + if (!fs.existsSync(config.tts.cachePath)) { + fs.mkdirSync(config.tts.cachePath, { recursive: true }); + } + + // Guardar texto en archivo temporal + const tempTextFile = path.join(config.tts.cachePath, `${hash}.txt`); + fs.writeFileSync(tempTextFile, cleanText, 'utf-8'); + + try { + const command = `cat "${tempTextFile}" | "${config.tts.piperPath}" --model "${voiceModel}" --output_file "${filePath}" 2>/dev/null`; + await execAsync(command, { timeout: 120000 }); + + // Limpiar archivo temporal + if (fs.existsSync(tempTextFile)) { + fs.unlinkSync(tempTextFile); + } + + if (fs.existsSync(filePath)) { + const stats = fs.statSync(filePath); + console.log(` ✅ Audio generado: ${fileName} (${Math.round(stats.size/1024)}KB)`); + return `${config.tts.baseUrl}/api/v1/tts/audio/${fileName}`; + } + + return null; + } catch (error) { + console.log(` ❌ Error TTS: ${error.message}`); + if (fs.existsSync(tempTextFile)) { + fs.unlinkSync(tempTextFile); + } + return null; + } +} + +async function main() { + console.log('🎙️ Generador de Audios TTS para Monumentos'); + console.log('==========================================\n'); + + // Verificar Piper + if (!fs.existsSync(config.tts.piperPath)) { + console.error(`❌ Piper no encontrado en: ${config.tts.piperPath}`); + process.exit(1); + } + console.log('✅ Piper TTS encontrado\n'); + + // Conectar a la DB + const client = new Client(config.db); + await client.connect(); + console.log('✅ Conectado a la base de datos\n'); + + // Obtener lugares con descripción + const query = ` + SELECT id, name, slug, + description_es, description_en, description_fr, description_it, description_de, + audio_url_es, audio_url_en, audio_url_fr, audio_url_it, audio_url_de + FROM tourism.places_of_interest + WHERE active = true + ORDER BY id + `; + + const result = await client.query(query); + const places = result.rows; + console.log(`📍 Encontrados ${places.length} lugares\n`); + + let totalGenerated = 0; + let totalSkipped = 0; + let totalFailed = 0; + + for (let i = 0; i < places.length; i++) { + const place = places[i]; + console.log(`\n[${i + 1}/${places.length}] ${place.name}`); + + for (const lang of config.languages) { + const descCol = `description_${lang}`; + const audioCol = `audio_url_${lang}`; + + const description = place[descCol]; + const existingAudio = place[audioCol]; + + if (!description) { + continue; + } + + if (existingAudio) { + totalSkipped++; + continue; + } + + console.log(` 🔊 Generando audio (${lang})...`); + const audioUrl = await generateAudio(description, lang); + + if (audioUrl) { + // Actualizar en DB + await client.query( + `UPDATE tourism.places_of_interest SET ${audioCol} = $1 WHERE id = $2`, + [audioUrl, place.id] + ); + totalGenerated++; + } else { + totalFailed++; + } + + // Pequeña pausa + await new Promise(r => setTimeout(r, 100)); + } + } + + await client.end(); + + console.log('\n=========================================='); + console.log('📊 RESUMEN'); + console.log(` ✅ Generados: ${totalGenerated}`); + console.log(` ⏭️ Omitidos (ya existían): ${totalSkipped}`); + console.log(` ❌ Fallidos: ${totalFailed}`); + console.log('==========================================\n'); +} + +main().catch(err => { + console.error('Error fatal:', err); + process.exit(1); +}); diff --git a/scripts/generate-missing.js b/scripts/generate-missing.js new file mode 100755 index 0000000..3a0e56d --- /dev/null +++ b/scripts/generate-missing.js @@ -0,0 +1,332 @@ +#!/usr/bin/env node +/** + * Script para generar descripciones y audios FALTANTES + */ + +const { Client } = require('pg'); +const { exec } = require('child_process'); +const { promisify } = require('util'); +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const https = require('https'); + +const execAsync = promisify(exec); + +// Configuración +const config = { + db: { + host: 'localhost', + port: 5432, + user: 'karibeo', + password: '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', + database: 'karibeo_db', + }, + kimi: { + apiKey: process.env.KIMI_API_KEY || '', + baseUrl: 'https://api.moonshot.ai/v1', + model: 'moonshot-v1-128k', + }, + tts: { + piperPath: '/home/karibeo-api/karibeo-api/tts/piper/piper', + voicesPath: '/home/karibeo-api/karibeo-api/tts/voices', + cachePath: '/home/karibeo-api/karibeo-api/tts/cache', + baseUrl: 'https://api.karibeo.ai:8443', + }, + voiceMap: { + es: 'es_ES-davefx-medium', + en: 'en_US-amy-medium', + fr: 'fr_FR-siwis-medium', + it: 'it_IT-riccardo-x_low', + de: 'de_DE-thorsten-medium', + }, + languages: ['es', 'en', 'fr', 'it', 'de'], +}; + +// Prompts por idioma +function getPrompt(placeName, country, language) { + const countryName = country === 'DO' ? 'República Dominicana' : 'Puerto Rico'; + const countryNameEn = country === 'DO' ? 'Dominican Republic' : 'Puerto Rico'; + + const prompts = { + es: `Actúa como un guía turístico ${country === 'DO' ? 'dominicano' : 'puertorriqueño'} experto. Dame una descripción completa e interesante de ${placeName} en ${countryName}. Incluye su historia, importancia cultural, arquitectura, datos curiosos, horarios de visita si aplica. Habla en segunda persona directamente al turista usando "tú" (NO uses "vosotros" ni expresiones de España). Usa español latinoamericano natural. Máximo 250 palabras.`, + + en: `Act as an expert ${country === 'DO' ? 'Dominican' : 'Puerto Rican'} tour guide. Give me a complete and interesting description of ${placeName} in ${countryNameEn}. Include its history, cultural importance, architecture, fun facts, and visiting hours if applicable. Speak in second person as if you were giving a tour. Maximum 250 words.`, + + fr: `Agis comme un guide touristique ${country === 'DO' ? 'dominicain' : 'portoricain'} expert. Donne-moi une description complète et intéressante de ${placeName} en ${countryName}. Inclus son histoire, son importance culturelle, son architecture, des anecdotes et les horaires de visite si applicable. Parle à la deuxième personne comme si tu donnais une visite guidée. Maximum 250 mots.`, + + it: `Agisci come una guida turistica ${country === 'DO' ? 'dominicana' : 'portoricana'} esperta. Dammi una descrizione completa e interessante di ${placeName} in ${countryName}. Includi la sua storia, importanza culturale, architettura, curiosità e orari di visita se applicabile. Parla in seconda persona come se stessi facendo un tour. Massimo 250 parole.`, + + de: `Handle als erfahrener ${country === 'DO' ? 'dominikanischer' : 'puertoricanischer'} Reiseführer. Gib mir eine vollständige und interessante Beschreibung von ${placeName} in ${countryName}. Füge die Geschichte, kulturelle Bedeutung, Architektur, interessante Fakten und Besuchszeiten falls zutreffend hinzu. Sprich in der zweiten Person, als würdest du eine Tour geben. Maximal 250 Wörter.`, + }; + + return prompts[language] || prompts['en']; +} + +function getLanguageName(code) { + const names = { + es: 'español latinoamericano', + en: 'English', + fr: 'français', + it: 'italiano', + de: 'Deutsch', + }; + return names[code] || 'English'; +} + +// Llamar a Kimi API +async function callKimiAPI(placeName, country, language) { + const prompt = getPrompt(placeName, country, language); + + const data = JSON.stringify({ + model: config.kimi.model, + messages: [ + { + role: 'system', + content: `Eres un guía turístico experto del Caribe. Responde siempre en ${getLanguageName(language)}. Sé informativo, amigable y entusiasta. No uses emojis.`, + }, + { + role: 'user', + content: prompt, + }, + ], + temperature: 0.7, + max_tokens: 1000, + }); + + return new Promise((resolve, reject) => { + const url = new URL(`${config.kimi.baseUrl}/chat/completions`); + const options = { + hostname: url.hostname, + port: 443, + path: url.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${config.kimi.apiKey}`, + 'Content-Length': Buffer.byteLength(data), + }, + }; + + const req = https.request(options, (res) => { + let body = ''; + res.on('data', (chunk) => body += chunk); + res.on('end', () => { + try { + const json = JSON.parse(body); + if (json.choices && json.choices[0] && json.choices[0].message) { + resolve(json.choices[0].message.content); + } else { + reject(new Error('Invalid response: ' + body)); + } + } catch (e) { + reject(e); + } + }); + }); + + req.on('error', reject); + req.setTimeout(60000, () => { + req.destroy(); + reject(new Error('Request timeout')); + }); + req.write(data); + req.end(); + }); +} + +// Limpiar texto para TTS +function cleanTextForSpeech(text) { + return text + .replace(/[\u4E00-\u9FFF]/g, '') + .replace(/[\u3400-\u4DBF]/g, '') + .replace(/[\u3040-\u309F]/g, '') + .replace(/[\u30A0-\u30FF]/g, '') + .replace(/[\uAC00-\uD7AF]/g, '') + .replace(/[\u{1F300}-\u{1F9FF}]/gu, '') + .replace(/[\u{2600}-\u{26FF}]/gu, '') + .replace(/[\u{2700}-\u{27BF}]/gu, '') + .replace(/[\u{1F600}-\u{1F64F}]/gu, '') + .replace(/[\u{1F680}-\u{1F6FF}]/gu, '') + .replace(/[\u{1F1E0}-\u{1F1FF}]/gu, '') + .replace(/[★☆✓✗✔✘●○◆◇▪▫►◄→←↑↓⇒⇐⇑⇓♠♣♥♦]/g, '') + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/\*([^*]+)\*/g, '$1') + .replace(/__([^_]+)__/g, '$1') + .replace(/_([^_]+)_/g, '$1') + .replace(/\s+/g, ' ') + .trim(); +} + +function getAudioHash(text, language) { + const content = `${language}:${text}`; + return crypto.createHash('md5').update(content).digest('hex'); +} + +async function generateAudio(text, language) { + const voice = config.voiceMap[language] || config.voiceMap['en']; + const voiceModel = path.join(config.tts.voicesPath, `${voice}.onnx`); + + if (!fs.existsSync(voiceModel)) { + console.log(` ⚠️ Modelo de voz no encontrado: ${voiceModel}`); + return null; + } + + const cleanText = cleanTextForSpeech(text) + .replace(/[\n\r]/g, ' ') + .replace(/"/g, "'") + .substring(0, 3000); + + const hash = getAudioHash(cleanText, language); + const fileName = `${hash}.wav`; + const filePath = path.join(config.tts.cachePath, fileName); + + if (fs.existsSync(filePath)) { + console.log(` 📦 Cache hit: ${fileName}`); + return `${config.tts.baseUrl}/api/v1/tts/audio/${fileName}`; + } + + if (!fs.existsSync(config.tts.cachePath)) { + fs.mkdirSync(config.tts.cachePath, { recursive: true }); + } + + const tempTextFile = path.join(config.tts.cachePath, `${hash}.txt`); + fs.writeFileSync(tempTextFile, cleanText, 'utf-8'); + + try { + const command = `cat "${tempTextFile}" | "${config.tts.piperPath}" --model "${voiceModel}" --output_file "${filePath}" 2>/dev/null`; + await execAsync(command, { timeout: 120000 }); + + if (fs.existsSync(tempTextFile)) { + fs.unlinkSync(tempTextFile); + } + + if (fs.existsSync(filePath)) { + const stats = fs.statSync(filePath); + console.log(` ✅ Audio generado: ${fileName} (${Math.round(stats.size/1024)}KB)`); + return `${config.tts.baseUrl}/api/v1/tts/audio/${fileName}`; + } + + return null; + } catch (error) { + console.log(` ❌ Error TTS: ${error.message}`); + if (fs.existsSync(tempTextFile)) { + fs.unlinkSync(tempTextFile); + } + return null; + } +} + +async function main() { + console.log('🔧 Generador de Contenido Faltante'); + console.log('===================================\n'); + + // Verificar API key + if (!config.kimi.apiKey) { + // Leer del .env + const envPath = '/home/karibeo-api/karibeo-api/.env'; + if (fs.existsSync(envPath)) { + const envContent = fs.readFileSync(envPath, 'utf-8'); + const match = envContent.match(/KIMI_API_KEY=(.+)/); + if (match) { + config.kimi.apiKey = match[1].trim(); + } + } + } + + if (!config.kimi.apiKey) { + console.error('❌ KIMI_API_KEY no encontrada'); + process.exit(1); + } + console.log('✅ Kimi API Key encontrada\n'); + + // Conectar a la DB + const client = new Client(config.db); + await client.connect(); + console.log('✅ Conectado a la base de datos\n'); + + // Obtener lugares con contenido faltante + const query = ` + SELECT id, name, country, + description_es, description_en, description_fr, description_it, description_de, + audio_url_es, audio_url_en, audio_url_fr, audio_url_it, audio_url_de + FROM tourism.places_of_interest + WHERE active = true + AND ( + description_es IS NULL OR description_en IS NULL OR description_fr IS NULL OR description_it IS NULL OR description_de IS NULL + OR audio_url_es IS NULL OR audio_url_en IS NULL OR audio_url_fr IS NULL OR audio_url_it IS NULL OR audio_url_de IS NULL + ) + ORDER BY name + `; + + const result = await client.query(query); + const places = result.rows; + console.log(`📍 Encontrados ${places.length} lugares con contenido faltante\n`); + + let descGenerated = 0; + let audioGenerated = 0; + let errors = 0; + + for (let i = 0; i < places.length; i++) { + const place = places[i]; + console.log(`\n[${i + 1}/${places.length}] ${place.name}`); + + for (const lang of config.languages) { + const descCol = `description_${lang}`; + const audioCol = `audio_url_${lang}`; + + let description = place[descCol]; + let audioUrl = place[audioCol]; + + // Generar descripción si falta + if (!description) { + console.log(` 📝 Generando descripción (${lang})...`); + try { + description = await callKimiAPI(place.name, place.country || 'DO', lang); + if (description) { + await client.query( + `UPDATE tourism.places_of_interest SET ${descCol} = $1 WHERE id = $2`, + [description, place.id] + ); + console.log(` ✅ Descripción generada (${lang})`); + descGenerated++; + } + } catch (error) { + console.log(` ❌ Error descripción (${lang}): ${error.message}`); + errors++; + } + await new Promise(r => setTimeout(r, 1000)); // Rate limit + } + + // Generar audio si falta y tiene descripción + if (description && !audioUrl) { + console.log(` 🔊 Generando audio (${lang})...`); + audioUrl = await generateAudio(description, lang); + if (audioUrl) { + await client.query( + `UPDATE tourism.places_of_interest SET ${audioCol} = $1 WHERE id = $2`, + [audioUrl, place.id] + ); + audioGenerated++; + } else { + errors++; + } + await new Promise(r => setTimeout(r, 100)); + } + } + } + + await client.end(); + + console.log('\n==================================='); + console.log('📊 RESUMEN'); + console.log(` 📝 Descripciones generadas: ${descGenerated}`); + console.log(` 🔊 Audios generados: ${audioGenerated}`); + console.log(` ❌ Errores: ${errors}`); + console.log('===================================\n'); +} + +main().catch(err => { + console.error('Error fatal:', err); + process.exit(1); +}); diff --git a/scripts/seed-monuments-part2.sql b/scripts/seed-monuments-part2.sql new file mode 100644 index 0000000..acbc459 --- /dev/null +++ b/scripts/seed-monuments-part2.sql @@ -0,0 +1,226 @@ +-- Script para poblar monumentos adicionales de RD y Puerto Rico (Parte 2) +-- Coordenadas en formato PostgreSQL point: (longitude, latitude) + +-- ========================================== +-- REPÚBLICA DOMINICANA - ZONA COLONIAL (Adicionales) +-- ========================================== + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Casa de la Moneda', 'casa-de-la-moneda', 'museum', point(-69.88282, 18.47353), 'DO', 'Calle Las Damas, Zona Colonial, Santo Domingo', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Reloj de Sol', 'reloj-de-sol', 'monument', point(-69.88235, 18.47481), 'DO', 'Calle Las Damas, Zona Colonial, Santo Domingo', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Casa del Cordón', 'casa-del-cordon', 'monument', point(-69.88312, 18.47594), 'DO', 'Calle Isabel la Católica, Zona Colonial, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Casa de Tostado', 'casa-de-tostado', 'museum', point(-69.88264, 18.47167), 'DO', 'Calle Padre Billini, Zona Colonial, Santo Domingo', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Iglesia de la Merced', 'iglesia-de-la-merced', 'church', point(-69.88724, 18.47398), 'DO', 'Calle Las Mercedes, Zona Colonial, Santo Domingo', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Iglesia de Regina Angelorum', 'iglesia-regina-angelorum', 'church', point(-69.88602, 18.47055), 'DO', 'Calle Padre Billini, Zona Colonial, Santo Domingo', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Ruinas del Monasterio de San Francisco', 'ruinas-monasterio-san-francisco', 'ruins', point(-69.88478, 18.47648), 'DO', 'Calle Hostos, Zona Colonial, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Casa de las Gárgolas', 'casa-de-las-gargolas', 'monument', point(-69.88272, 18.47504), 'DO', 'Calle Las Damas, Zona Colonial, Santo Domingo', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Museo del Ámbar', 'museo-del-ambar', 'museum', point(-69.88651, 18.47402), 'DO', 'Calle Arzobispo Meriño, Zona Colonial, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Museo de Larimar', 'museo-de-larimar', 'museum', point(-69.88295, 18.47185), 'DO', 'Calle Isabel la Católica, Zona Colonial, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- ========================================== +-- SANTO DOMINGO (Fuera de Zona Colonial) +-- ========================================== + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Malecón de Santo Domingo', 'malecon-santo-domingo', 'promenade', point(-69.89381, 18.46351), 'DO', 'Av. George Washington, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Plaza de la Cultura', 'plaza-de-la-cultura', 'plaza', point(-69.91102, 18.47141), 'DO', 'Av. Máximo Gómez, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Palacio de Bellas Artes', 'palacio-bellas-artes', 'museum', point(-69.90956, 18.46654), 'DO', 'Av. Máximo Gómez, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Teatro Nacional Eduardo Brito', 'teatro-nacional', 'theater', point(-69.91008, 18.47172), 'DO', 'Plaza de la Cultura, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Centro Olímpico Juan Pablo Duarte', 'centro-olimpico', 'sports', point(-69.91605, 18.48002), 'DO', 'Av. 27 de Febrero, Santo Domingo', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- ========================================== +-- RD - NORTE (Puerto Plata) +-- ========================================== + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('27 Charcos de Damajagua', '27-charcos-damajagua', 'natural', point(-70.82471, 19.72885), 'DO', 'Imbert, Puerto Plata', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Ocean World Adventure Park', 'ocean-world', 'attraction', point(-70.73155, 19.83152), 'DO', 'Cofresí, Puerto Plata', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Playa Dorada', 'playa-dorada', 'beach', point(-70.64402, 19.76801), 'DO', 'Puerto Plata', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- ========================================== +-- RD - ESTE (Punta Cana, Hato Mayor) +-- ========================================== + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Cueva Fun Fun', 'cueva-fun-fun', 'natural', point(-69.44405, 19.04802), 'DO', 'Hato Mayor', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Hoyo Azul', 'hoyo-azul', 'natural', point(-68.45502, 18.44855), 'DO', 'Scape Park, Cap Cana, Punta Cana', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Indigenous Eyes Ecological Park', 'indigenous-eyes', 'natural', point(-68.37504, 18.51301), 'DO', 'Puntacana Resort & Club', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Playa Bávaro', 'playa-bavaro', 'beach', point(-68.41805, 18.68002), 'DO', 'Bávaro, Punta Cana', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Parque Nacional Los Haitises', 'parque-los-haitises', 'natural', point(-69.51672, 19.06671), 'DO', 'Sabana de la Mar, Hato Mayor', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- ========================================== +-- RD - SUROESTE (Barahona, Pedernales) +-- ========================================== + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Lago Enriquillo', 'lago-enriquillo', 'natural', point(-71.65002, 18.48405), 'DO', 'Independencia / Bahoruco', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Isla Cabritos', 'isla-cabritos', 'natural', point(-71.68504, 18.48802), 'DO', 'Lago Enriquillo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Bahía de las Águilas', 'bahia-de-las-aguilas', 'beach', point(-71.64205, 17.86402), 'DO', 'Parque Nacional Jaragua, Pedernales', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Parque Nacional Jaragua', 'parque-jaragua', 'natural', point(-71.50004, 17.85002), 'DO', 'Oviedo, Pedernales', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- ========================================== +-- RD - MONTAÑAS (Constanza, Jarabacoa) +-- ========================================== + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Aguas Blancas', 'aguas-blancas', 'natural', point(-70.67505, 18.85002), 'DO', 'Constanza, La Vega', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Piedra Letrada', 'piedra-letrada', 'natural', point(-70.76204, 18.78852), 'DO', 'Constanza, La Vega', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- ========================================== +-- PUERTO RICO - SAN JUAN (Adicionales) +-- ========================================== + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Museo de Arte de Puerto Rico', 'museo-arte-puerto-rico', 'museum', point(-66.06652, 18.44825), 'PR', 'Av. De Diego 299, Santurce, San Juan', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Fuerte San Juan de la Cruz (El Cañuelo)', 'fuerte-el-canuelo', 'fortress', point(-66.13601, 18.47352), 'PR', 'Isla de Cabras, Toa Baja', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Plaza de la Rogativa', 'plaza-de-la-rogativa', 'plaza', point(-66.11972, 18.46685), 'PR', 'Caleta de las Monjas, Viejo San Juan', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Fuente Raíces', 'fuente-raices', 'monument', point(-66.11865, 18.46382), 'PR', 'Paseo de la Princesa, Viejo San Juan', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- ========================================== +-- PUERTO RICO - COSTA NORTE +-- ========================================== + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Observatorio de Arecibo', 'observatorio-arecibo', 'museum', point(-66.75282, 18.34421), 'PR', 'Carretera 625, Arecibo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Playa Crash Boat', 'playa-crash-boat', 'beach', point(-67.16301, 18.45802), 'PR', 'Aguadilla', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Parque de las Cavernas del Río Camuy', 'cavernas-rio-camuy', 'natural', point(-66.82405, 18.34702), 'PR', 'Carretera 129, Camuy', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- ========================================== +-- PUERTO RICO - COSTA SUR +-- ========================================== + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Porta Coeli', 'porta-coeli', 'church', point(-67.04021, 18.08182), 'PR', 'San Germán', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('La Parguera', 'la-parguera', 'natural', point(-67.04505, 17.97202), 'PR', 'Lajas', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Bosque Seco de Guánica', 'bosque-seco-guanica', 'natural', point(-66.86554, 17.97152), 'PR', 'Guánica', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- ========================================== +-- PUERTO RICO - CENTRO / MONTAÑAS +-- ========================================== + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Toro Verde Adventure Park', 'toro-verde', 'attraction', point(-66.39101, 18.25202), 'PR', 'Orocovis', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Hacienda Buena Vista', 'hacienda-buena-vista', 'museum', point(-66.65481, 18.08442), 'PR', 'Ponce', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Bosque Estatal de Toro Negro', 'bosque-toro-negro', 'natural', point(-66.58784, 18.17252), 'PR', 'Jayuya/Ciales', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- ========================================== +-- PUERTO RICO - ISLAS +-- ========================================== + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Faro de Culebrita', 'faro-culebrita', 'monument', point(-65.23004, 18.31502), 'PR', 'Isla Culebrita, Culebra', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Sun Bay Beach', 'sun-bay-beach', 'beach', point(-65.46205, 18.10002), 'PR', 'Vieques', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- Mostrar resultado +SELECT 'Parte 2 completada!' as status, COUNT(*) as total_places FROM tourism.places_of_interest WHERE active = true; +SELECT country, COUNT(*) as cantidad FROM tourism.places_of_interest WHERE active = true GROUP BY country ORDER BY country; diff --git a/scripts/seed-monuments.sql b/scripts/seed-monuments.sql new file mode 100644 index 0000000..446015a --- /dev/null +++ b/scripts/seed-monuments.sql @@ -0,0 +1,226 @@ +-- Script para poblar monumentos de RD y Puerto Rico +-- Coordenadas en formato PostgreSQL point: (longitude, latitude) + +-- Agregar unique constraint en slug si no existe +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'places_of_interest_slug_key') THEN + ALTER TABLE tourism.places_of_interest ADD CONSTRAINT places_of_interest_slug_key UNIQUE (slug); + END IF; +EXCEPTION + WHEN duplicate_object THEN NULL; +END $$; + +-- REPÚBLICA DOMINICANA - ZONA COLONIAL +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Catedral Primada de América', 'catedral-primada-de-america', 'monument', point(-69.884250, 18.472988), 'DO', 'Calle Arzobispo Meriño, Zona Colonial, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Fortaleza Ozama', 'fortaleza-ozama', 'fortress', point(-69.8817, 18.4732), 'DO', 'Calle Las Damas, Zona Colonial, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Alcázar de Colón', 'alcazar-de-colon', 'palace', point(-69.8832, 18.4775), 'DO', 'Plaza de España, Zona Colonial, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Faro a Colón', 'faro-a-colon', 'monument', point(-69.8682, 18.4786), 'DO', 'Av. España, Santo Domingo Este', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Panteón Nacional', 'panteon-nacional', 'monument', point(-69.8845, 18.4735), 'DO', 'Calle Las Damas, Zona Colonial, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Parque Colón', 'parque-colon', 'park', point(-69.8842, 18.4730), 'DO', 'Calle El Conde, Zona Colonial, Santo Domingo', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Calle Las Damas', 'calle-las-damas', 'street', point(-69.8820, 18.4738), 'DO', 'Zona Colonial, Santo Domingo', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Museo de las Casas Reales', 'museo-casas-reales', 'museum', point(-69.8825, 18.4742), 'DO', 'Calle Las Damas, Zona Colonial, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Puerta del Conde', 'puerta-del-conde', 'monument', point(-69.8955, 18.4692), 'DO', 'Parque Independencia, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Parque Independencia', 'parque-independencia', 'park', point(-69.8960, 18.4688), 'DO', 'Av. Bolívar, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Los Tres Ojos', 'los-tres-ojos', 'natural', point(-69.8545, 18.4695), 'DO', 'Parque Mirador del Este, Santo Domingo Este', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Jardín Botánico Nacional', 'jardin-botanico-nacional', 'park', point(-69.9467, 18.4950), 'DO', 'Av. República de Colombia, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Palacio Nacional', 'palacio-nacional', 'palace', point(-69.9140, 18.4750), 'DO', 'Av. México, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Ruinas del Hospital San Nicolás de Bari', 'ruinas-hospital-san-nicolas', 'ruins', point(-69.8852, 18.4722), 'DO', 'Calle Hostos, Zona Colonial, Santo Domingo', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Iglesia de Santa Bárbara', 'iglesia-santa-barbara', 'church', point(-69.8812, 18.4755), 'DO', 'Calle Isabel la Católica, Zona Colonial, Santo Domingo', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Convento de los Dominicos', 'convento-dominicos', 'monument', point(-69.8868, 18.4725), 'DO', 'Calle Padre Billini, Zona Colonial, Santo Domingo', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Plaza de España', 'plaza-de-espana', 'plaza', point(-69.8828, 18.4770), 'DO', 'Zona Colonial, Santo Domingo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- RD - SANTIAGO +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Monumento a los Héroes de la Restauración', 'monumento-heroes-restauracion', 'monument', point(-70.6931, 19.4792), 'DO', 'Centro de Santiago de los Caballeros', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- RD - PUERTO PLATA +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Fortaleza San Felipe', 'fortaleza-san-felipe', 'fortress', point(-70.6940, 19.7982), 'DO', 'Puerto Plata', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Teleférico de Puerto Plata', 'teleferico-puerto-plata', 'attraction', point(-70.6875, 19.7842), 'DO', 'Monte Isabel de Torres, Puerto Plata', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- RD - LA ROMANA +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Altos de Chavón', 'altos-de-chavon', 'village', point(-68.9620, 18.4270), 'DO', 'Casa de Campo, La Romana', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- RD - SAMANÁ +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Salto El Limón', 'salto-el-limon', 'natural', point(-69.4420, 19.2850), 'DO', 'El Limón, Samaná', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Playa Rincón', 'playa-rincon', 'beach', point(-69.2385, 19.2120), 'DO', 'Samaná', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Cayo Levantado', 'cayo-levantado', 'island', point(-69.3840, 19.1750), 'DO', 'Bahía de Samaná', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- RD - HIGÜEY +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Basílica Nuestra Señora de la Altagracia', 'basilica-altagracia', 'church', point(-68.7108, 18.6155), 'DO', 'Higüey, La Altagracia', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- RD - CONSTANZA +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Valle Nuevo', 'valle-nuevo', 'natural', point(-70.6020, 18.7880), 'DO', 'Constanza, La Vega', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- RD - JARABACOA +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Salto de Jimenoa', 'salto-jimenoa', 'natural', point(-70.6210, 19.1320), 'DO', 'Jarabacoa, La Vega', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Pico Duarte', 'pico-duarte', 'natural', point(-70.9893, 19.0291), 'DO', 'Cordillera Central', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- RD - BAYAHIBE +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Isla Saona', 'isla-saona', 'island', point(-68.7280, 18.1550), 'DO', 'Parque Nacional del Este', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- ========================================== +-- PUERTO RICO - VIEJO SAN JUAN +-- ========================================== +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Castillo San Felipe del Morro', 'castillo-san-felipe-del-morro', 'fortress', point(-66.1212, 18.4693), 'PR', '501 Calle Norzagaray, San Juan', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Castillo San Cristóbal', 'castillo-san-cristobal', 'fortress', point(-66.1067, 18.4670), 'PR', 'Calle Norzagaray, San Juan', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('La Fortaleza', 'la-fortaleza', 'palace', point(-66.1190, 18.4641), 'PR', '63 Calle de la Fortaleza, San Juan', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Catedral de San Juan Bautista', 'catedral-san-juan-bautista', 'church', point(-66.1170, 18.4660), 'PR', '153 Calle del Cristo, San Juan', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Capilla del Cristo', 'capilla-del-cristo', 'church', point(-66.1195, 18.4636), 'PR', 'Calle del Cristo, San Juan', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Casa Blanca', 'casa-blanca-pr', 'museum', point(-66.1185, 18.4685), 'PR', '1 Calle San Sebastián, San Juan', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Paseo de la Princesa', 'paseo-de-la-princesa', 'promenade', point(-66.1175, 18.4620), 'PR', 'Paseo de la Princesa, San Juan', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Puerta de San Juan', 'puerta-de-san-juan', 'monument', point(-66.1200, 18.4645), 'PR', 'Recinto Sur, San Juan', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Cementerio Santa María Magdalena de Pazzis', 'cementerio-santa-maria-pazzis', 'cemetery', point(-66.1225, 18.4705), 'PR', 'Calle Norzagaray, San Juan', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Plaza de Armas', 'plaza-de-armas-pr', 'plaza', point(-66.1155, 18.4655), 'PR', 'Calle San Francisco, San Juan', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Parque de las Palomas', 'parque-de-las-palomas', 'park', point(-66.1190, 18.4635), 'PR', 'Final Calle del Cristo, San Juan', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- PUERTO RICO - PONCE +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Parque de Bombas', 'parque-de-bombas', 'museum', point(-66.6142, 18.0125), 'PR', 'Plaza Las Delicias, Ponce', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Catedral Nuestra Señora de Guadalupe', 'catedral-ponce', 'church', point(-66.6138, 18.0128), 'PR', 'Plaza Las Delicias, Ponce', false, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Castillo Serrallés', 'castillo-serralles', 'palace', point(-66.6285, 18.0205), 'PR', 'El Vigía, Ponce', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- PUERTO RICO - NATURALEZA +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('El Yunque National Forest', 'el-yunque', 'natural', point(-65.7847, 18.3154), 'PR', 'Río Grande', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Bahía Bioluminiscente de Vieques', 'bahia-bioluminiscente-vieques', 'natural', point(-65.4775, 18.0965), 'PR', 'Mosquito Bay, Vieques', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Cueva del Indio', 'cueva-del-indio', 'natural', point(-66.6375, 18.4835), 'PR', 'Arecibo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Flamenco Beach', 'flamenco-beach', 'beach', point(-65.3180, 18.3290), 'PR', 'Culebra', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Bahía Bioluminiscente de Fajardo', 'laguna-grande-fajardo', 'natural', point(-65.6350, 18.3640), 'PR', 'Las Croabas, Fajardo', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +INSERT INTO tourism.places_of_interest (name, slug, category, coordinates, country, address, featured, active, created_at, updated_at) +VALUES ('Cañón San Cristóbal', 'canon-san-cristobal', 'natural', point(-66.3510, 18.1890), 'PR', 'Barranquitas/Aibonito', true, true, NOW(), NOW()) +ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, coordinates = EXCLUDED.coordinates, address = EXCLUDED.address, updated_at = NOW(); + +-- Mostrar resultado +SELECT 'Seeding completado!' as status, COUNT(*) as total_places FROM tourism.places_of_interest WHERE active = true; +SELECT country, COUNT(*) as cantidad FROM tourism.places_of_interest WHERE active = true GROUP BY country; diff --git a/src/app.module.ts b/src/app.module.ts index 789b68d..494f60d 100755 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,6 +13,7 @@ import appConfig from './config/app.config'; import stripeConfig from './config/integrations/stripe.config'; import awsConfig from './config/integrations/aws.config'; import communicationConfig from './config/integrations/communication.config'; +import kimiConfig from './config/integrations/kimi.config'; // Entity imports import { User } from './entities/user.entity'; @@ -76,6 +77,10 @@ import { Listing } from './entities/listing.entity'; import { Vehicle } from './entities/vehicle.entity'; import { Flight } from './entities/flight.entity'; import { Availability } from './entities/availability.entity'; +import { UserFavorite } from './entities/user-favorite.entity'; +import { UserCollection, CollectionItem } from './entities/user-collection.entity'; +import { UserTrip, TripDay, TripActivity } from './entities/user-trip.entity'; +import { TravelQuizResponse } from './entities/travel-quiz.entity'; // Module imports @@ -92,6 +97,7 @@ import { CommunicationModule } from './modules/communication/communication.modul import { RestaurantModule } from './modules/restaurant/restaurant.module'; import { HotelModule } from './modules/hotel/hotel.module'; import { AIGuideModule } from './modules/ai-guide/ai-guide.module'; +import { KimiModule } from './modules/kimi/kimi.module'; import { GeolocationModule } from './modules/geolocation/geolocation.module'; import { ReviewsModule } from './modules/reviews/reviews.module'; import { AIGeneratorModule } from './modules/ai-generator/ai-generator.module'; @@ -105,7 +111,14 @@ import { ChannelManagementModule } from './modules/channel-management/channel-ma import { ListingsModule } from './modules/listings/listings.module'; import { VehicleManagementModule } from './modules/vehicle-management/vehicle-management.module'; import { FlightManagementModule } from './modules/flight-management/flight-management.module'; +import { BookingModule } from './modules/booking/booking.module'; +import { UnifiedSearchModule } from './modules/unified-search/unified-search.module'; import { AvailabilityManagementModule } from './modules/availability-management/availability-management.module'; +import { FavoritesModule } from './modules/favorites/favorites.module'; +import { CollectionsModule } from './modules/collections/collections.module'; +import { TripsModule } from './modules/trips/trips.module'; +import { QuizModule } from './modules/quiz/quiz.module'; +import { ContentGeneratorModule } from './modules/content-generator/content-generator.module'; @Module({ @@ -120,6 +133,7 @@ import { AvailabilityManagementModule } from './modules/availability-management/ stripeConfig, awsConfig, communicationConfig, + kimiConfig, ], envFilePath: '.env', }), @@ -157,7 +171,7 @@ import { AvailabilityManagementModule } from './modules/availability-management/ // Finance entities CommissionRate, AdminTransaction, Settlement, // NUEVAS Entidades - Channel, Listing, Vehicle, Flight, Availability, + Channel, Listing, Vehicle, Flight, Availability, UserFavorite, UserCollection, CollectionItem, UserTrip, TripDay, TripActivity, TravelQuizResponse, ], }), inject: [ConfigService], @@ -200,6 +214,11 @@ import { AvailabilityManagementModule } from './modules/availability-management/ ChannelManagementModule, ListingsModule, AvailabilityManagementModule, + FavoritesModule, + CollectionsModule, + TripsModule, + QuizModule, + ContentGeneratorModule, // Integration modules (3) PaymentsModule, @@ -207,6 +226,7 @@ import { AvailabilityManagementModule } from './modules/availability-management/ CommunicationModule, // Advanced features modules (3) + KimiModule, AIGuideModule, GeolocationModule, ReviewsModule, @@ -214,6 +234,8 @@ import { AvailabilityManagementModule } from './modules/availability-management/ // Logistics & Booking Modules (2) - Nueva categoría VehicleManagementModule, FlightManagementModule, + BookingModule, + UnifiedSearchModule, // Innovation 2025 modules (5) AIGeneratorModule, diff --git a/src/config/integrations/booking.config.ts b/src/config/integrations/booking.config.ts new file mode 100644 index 0000000..0a74029 --- /dev/null +++ b/src/config/integrations/booking.config.ts @@ -0,0 +1,20 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('booking', () => ({ + // Booking.com Demand API credentials + affiliateId: process.env.BOOKING_AFFILIATE_ID || '', + apiKey: process.env.BOOKING_API_KEY || '', + + // API Base URL + baseUrl: process.env.BOOKING_BASE_URL || 'https://demandapi.booking.com/3.1', + + // Environment: 'sandbox' or 'production' + environment: process.env.BOOKING_ENVIRONMENT || 'sandbox', + + // Default settings + defaultCurrency: process.env.BOOKING_DEFAULT_CURRENCY || 'USD', + defaultLanguage: process.env.BOOKING_DEFAULT_LANGUAGE || 'es', + + // Rate limiting + maxRequestsPerSecond: parseInt(process.env.BOOKING_MAX_REQUESTS_PER_SECOND || '5', 10), +})); diff --git a/src/config/integrations/kimi.config.ts b/src/config/integrations/kimi.config.ts new file mode 100644 index 0000000..1075b73 --- /dev/null +++ b/src/config/integrations/kimi.config.ts @@ -0,0 +1,11 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('kimi', () => ({ + apiKey: process.env.KIMI_API_KEY, + baseUrl: process.env.KIMI_BASE_URL || 'https://api.moonshot.ai/v1', + model: process.env.KIMI_MODEL || 'kimi-k2.5', + mode: process.env.KIMI_MODE || 'instant', + temperature: parseFloat(process.env.KIMI_TEMPERATURE || '0.6'), + maxTokens: parseInt(process.env.KIMI_MAX_TOKENS || '4096', 10), + topP: parseFloat(process.env.KIMI_TOP_P || '0.95'), +})); diff --git a/src/entities/place-of-interest.entity.ts b/src/entities/place-of-interest.entity.ts index 1dbfff8..7961865 100755 --- a/src/entities/place-of-interest.entity.ts +++ b/src/entities/place-of-interest.entity.ts @@ -1,80 +1,138 @@ -import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { ApiProperty } from '@nestjs/swagger'; -import { BaseEntity } from './base.entity'; -import { Destination } from './destination.entity'; - -@Entity({ name: 'places_of_interest', schema: 'tourism' }) -export class PlaceOfInterest extends BaseEntity { - @ApiProperty({ description: 'Destination ID', example: 1 }) - @Column({ name: 'destination_id', nullable: true }) - destinationId: number; - - @ApiProperty({ description: 'Place name', example: 'Alcázar de Colón' }) - @Column({ length: 255 }) - name: string; - - @ApiProperty({ description: 'Place description' }) - @Column({ type: 'text', nullable: true }) - description: string; - - @ApiProperty({ description: 'Category', example: 'monument' }) - @Column({ length: 50, nullable: true }) - category: string; - - @ApiProperty({ description: 'Coordinates (lat, lng)' }) - @Column({ type: 'point' }) - coordinates: string; - - @ApiProperty({ description: 'Address', example: 'Plaza de Armas, Santo Domingo' }) - @Column({ type: 'text', nullable: true }) - address: string; - - @ApiProperty({ description: 'Phone number', example: '+1809555XXXX' }) - @Column({ length: 20, nullable: true }) - phone: string; - - @ApiProperty({ description: 'Website URL' }) - @Column({ length: 255, nullable: true }) - website: string; - - @ApiProperty({ description: 'Opening hours' }) - @Column({ name: 'opening_hours', type: 'jsonb', nullable: true }) - openingHours: Record; - - @ApiProperty({ description: 'Entrance fee', example: 25.00 }) - @Column({ name: 'entrance_fee', type: 'decimal', precision: 10, scale: 2, nullable: true }) - entranceFee: number; - - @ApiProperty({ description: 'Images' }) - @Column({ type: 'jsonb', nullable: true }) - images: Record; - - @ApiProperty({ description: 'Historical information' }) - @Column({ name: 'historical_info', type: 'text', nullable: true }) - historicalInfo: string; - - @ApiProperty({ description: 'AR content' }) - @Column({ name: 'ar_content', type: 'jsonb', nullable: true }) - arContent: Record; - - @ApiProperty({ description: 'Audio guide URL' }) - @Column({ name: 'audio_guide_url', type: 'text', nullable: true }) - audioGuideUrl: string; - - @ApiProperty({ description: 'Average rating', example: 4.5 }) - @Column({ type: 'decimal', precision: 3, scale: 2, nullable: true }) - rating: number; - - @ApiProperty({ description: 'Total reviews', example: 150 }) - @Column({ name: 'total_reviews', default: 0 }) - totalReviews: number; - - @ApiProperty({ description: 'Active status', example: true }) - @Column({ default: true }) - active: boolean; - - // Relations - @ManyToOne(() => Destination) - @JoinColumn({ name: 'destination_id' }) - destination: Destination; -} +import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { ApiProperty } from '@nestjs/swagger'; +import { BaseEntity } from './base.entity'; +import { Destination } from './destination.entity'; + +@Entity({ name: 'places_of_interest', schema: 'tourism' }) +export class PlaceOfInterest extends BaseEntity { + @ApiProperty({ description: 'Destination ID', example: 1 }) + @Column({ name: 'destination_id', nullable: true }) + destinationId: number; + + @ApiProperty({ description: 'Place name', example: 'Alcázar de Colón' }) + @Column({ length: 255 }) + name: string; + + @ApiProperty({ description: 'URL-friendly slug', example: 'alcazar-de-colon' }) + @Column({ length: 255, nullable: true }) + slug: string; + + @ApiProperty({ description: 'Place description (legacy)' }) + @Column({ type: 'text', nullable: true }) + description: string; + + // Multi-language descriptions + @ApiProperty({ description: 'Description in Spanish' }) + @Column({ name: 'description_es', type: 'text', nullable: true }) + descriptionEs: string; + + @ApiProperty({ description: 'Description in English' }) + @Column({ name: 'description_en', type: 'text', nullable: true }) + descriptionEn: string; + + @ApiProperty({ description: 'Description in French' }) + @Column({ name: 'description_fr', type: 'text', nullable: true }) + descriptionFr: string; + + @ApiProperty({ description: 'Description in Italian' }) + @Column({ name: 'description_it', type: 'text', nullable: true }) + descriptionIt: string; + + @ApiProperty({ description: 'Description in German' }) + @Column({ name: 'description_de', type: 'text', nullable: true }) + descriptionDe: string; + + // Multi-language audio URLs + @ApiProperty({ description: 'Audio URL in Spanish' }) + @Column({ name: 'audio_url_es', type: 'text', nullable: true }) + audioUrlEs: string; + + @ApiProperty({ description: 'Audio URL in English' }) + @Column({ name: 'audio_url_en', type: 'text', nullable: true }) + audioUrlEn: string; + + @ApiProperty({ description: 'Audio URL in French' }) + @Column({ name: 'audio_url_fr', type: 'text', nullable: true }) + audioUrlFr: string; + + @ApiProperty({ description: 'Audio URL in Italian' }) + @Column({ name: 'audio_url_it', type: 'text', nullable: true }) + audioUrlIt: string; + + @ApiProperty({ description: 'Audio URL in German' }) + @Column({ name: 'audio_url_de', type: 'text', nullable: true }) + audioUrlDe: string; + + @ApiProperty({ description: 'Voice ID for TTS consistency' }) + @Column({ name: 'voice_id', length: 50, nullable: true }) + voiceId: string; + + @ApiProperty({ description: 'Country code (DO, PR)', example: 'DO' }) + @Column({ length: 2, default: 'DO' }) + country: string; + + @ApiProperty({ description: 'Featured place', example: false }) + @Column({ default: false }) + featured: boolean; + + @ApiProperty({ description: 'Category', example: 'monument' }) + @Column({ length: 50, nullable: true }) + category: string; + + @ApiProperty({ description: 'Coordinates (lat, lng)' }) + @Column({ type: 'point' }) + coordinates: string; + + @ApiProperty({ description: 'Address', example: 'Plaza de Armas, Santo Domingo' }) + @Column({ type: 'text', nullable: true }) + address: string; + + @ApiProperty({ description: 'Phone number', example: '+1809555XXXX' }) + @Column({ length: 20, nullable: true }) + phone: string; + + @ApiProperty({ description: 'Website URL' }) + @Column({ length: 255, nullable: true }) + website: string; + + @ApiProperty({ description: 'Opening hours' }) + @Column({ name: 'opening_hours', type: 'jsonb', nullable: true }) + openingHours: Record; + + @ApiProperty({ description: 'Entrance fee', example: 25.00 }) + @Column({ name: 'entrance_fee', type: 'decimal', precision: 10, scale: 2, nullable: true }) + entranceFee: number; + + @ApiProperty({ description: 'Images' }) + @Column({ type: 'jsonb', nullable: true }) + images: Record; + + @ApiProperty({ description: 'Historical information' }) + @Column({ name: 'historical_info', type: 'text', nullable: true }) + historicalInfo: string; + + @ApiProperty({ description: 'AR content' }) + @Column({ name: 'ar_content', type: 'jsonb', nullable: true }) + arContent: Record; + + @ApiProperty({ description: 'Audio guide URL (legacy)' }) + @Column({ name: 'audio_guide_url', type: 'text', nullable: true }) + audioGuideUrl: string; + + @ApiProperty({ description: 'Average rating', example: 4.5 }) + @Column({ type: 'decimal', precision: 3, scale: 2, nullable: true }) + rating: number; + + @ApiProperty({ description: 'Total reviews', example: 150 }) + @Column({ name: 'total_reviews', default: 0 }) + totalReviews: number; + + @ApiProperty({ description: 'Active status', example: true }) + @Column({ default: true }) + active: boolean; + + // Relations + @ManyToOne(() => Destination) + @JoinColumn({ name: 'destination_id' }) + destination: Destination; +} diff --git a/src/entities/travel-quiz.entity.ts b/src/entities/travel-quiz.entity.ts new file mode 100644 index 0000000..01c2a79 --- /dev/null +++ b/src/entities/travel-quiz.entity.ts @@ -0,0 +1,69 @@ +import { Entity, Column, ManyToOne, JoinColumn, Index } from 'typeorm'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BaseEntity } from './base.entity'; +import { User } from './user.entity'; + +@Entity({ name: 'travel_quiz_responses', schema: 'analytics' }) +@Index(['userId'], { unique: true }) +export class TravelQuizResponse extends BaseEntity { + @ApiProperty({ description: 'User ID' }) + @Column({ name: 'user_id', type: 'uuid' }) + userId: string; + + @ApiProperty({ description: 'Travel styles', type: 'array' }) + @Column({ name: 'travel_styles', type: 'text', array: true, default: [] }) + travelStyles: string[]; + + @ApiProperty({ description: 'Preferred activities', type: 'array' }) + @Column({ name: 'preferred_activities', type: 'text', array: true, default: [] }) + preferredActivities: string[]; + + @ApiProperty({ description: 'Accommodation preferences', type: 'array' }) + @Column({ name: 'accommodation_preferences', type: 'text', array: true, default: [] }) + accommodationPreferences: string[]; + + @ApiProperty({ description: 'Budget range' }) + @Column({ name: 'budget_range', length: 50, nullable: true }) + budgetRange: string; + + @ApiProperty({ description: 'Trip duration preference' }) + @Column({ name: 'trip_duration', length: 50, nullable: true }) + tripDuration: string; + + @ApiProperty({ description: 'Group type', example: 'solo' }) + @Column({ name: 'group_type', length: 50, nullable: true }) + groupType: string; + + @ApiProperty({ description: 'Cuisine preferences', type: 'array' }) + @Column({ name: 'cuisine_preferences', type: 'text', array: true, default: [] }) + cuisinePreferences: string[]; + + @ApiProperty({ description: 'Interests', type: 'array' }) + @Column({ type: 'text', array: true, default: [] }) + interests: string[]; + + @ApiProperty({ description: 'Accessibility needs', type: 'array' }) + @Column({ name: 'accessibility_needs', type: 'text', array: true, default: [] }) + accessibilityNeeds: string[]; + + @ApiPropertyOptional({ description: 'AI-generated travel persona' }) + @Column({ name: 'travel_persona', length: 100, nullable: true }) + travelPersona: string; + + @ApiPropertyOptional({ description: 'Persona description' }) + @Column({ name: 'persona_description', type: 'text', nullable: true }) + personaDescription: string; + + @ApiProperty({ description: 'Quiz completed', default: false }) + @Column({ name: 'is_completed', default: false }) + isCompleted: boolean; + + @ApiPropertyOptional({ description: 'Completed at' }) + @Column({ name: 'completed_at', type: 'timestamp', nullable: true }) + completedAt: Date; + + // Relations + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; +} diff --git a/src/entities/user-collection.entity.ts b/src/entities/user-collection.entity.ts new file mode 100644 index 0000000..daa1c69 --- /dev/null +++ b/src/entities/user-collection.entity.ts @@ -0,0 +1,85 @@ +import { Entity, Column, ManyToOne, OneToMany, JoinColumn, Index } from 'typeorm'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BaseEntity } from './base.entity'; +import { User } from './user.entity'; + +@Entity({ name: 'user_collections', schema: 'tourism' }) +@Index(['userId', 'name'], { unique: true }) +export class UserCollection extends BaseEntity { + @ApiProperty({ description: 'User ID' }) + @Column({ name: 'user_id', type: 'uuid' }) + userId: string; + + @ApiProperty({ description: 'Collection name', example: 'My Beach Trip' }) + @Column({ length: 100 }) + name: string; + + @ApiPropertyOptional({ description: 'Collection description' }) + @Column({ type: 'text', nullable: true }) + description: string; + + @ApiPropertyOptional({ description: 'Cover image URL' }) + @Column({ name: 'cover_image_url', type: 'text', nullable: true }) + coverImageUrl: string; + + @ApiPropertyOptional({ description: 'Collection color theme', example: '#FF5722' }) + @Column({ length: 20, nullable: true }) + color: string; + + @ApiPropertyOptional({ description: 'Collection icon', example: 'beach' }) + @Column({ length: 50, nullable: true }) + icon: string; + + @ApiProperty({ description: 'Is public collection', default: false }) + @Column({ name: 'is_public', default: false }) + isPublic: boolean; + + @ApiProperty({ description: 'Sort order for display', default: 0 }) + @Column({ name: 'sort_order', default: 0 }) + sortOrder: number; + + // Relations + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @OneToMany(() => CollectionItem, item => item.collection) + items: CollectionItem[]; +} + +@Entity({ name: 'collection_items', schema: 'tourism' }) +@Index(['collectionId', 'itemId', 'itemType'], { unique: true }) +export class CollectionItem extends BaseEntity { + @ApiProperty({ description: 'Collection ID' }) + @Column({ name: 'collection_id', type: 'uuid' }) + collectionId: string; + + @ApiProperty({ description: 'Item ID' }) + @Column({ name: 'item_id', type: 'uuid' }) + itemId: string; + + @ApiProperty({ description: 'Item type', example: 'place' }) + @Column({ name: 'item_type', length: 50 }) + itemType: string; + + @ApiPropertyOptional({ description: 'Item name (cached)' }) + @Column({ name: 'item_name', length: 255, nullable: true }) + itemName: string; + + @ApiPropertyOptional({ description: 'Item image URL (cached)' }) + @Column({ name: 'item_image_url', type: 'text', nullable: true }) + itemImageUrl: string; + + @ApiPropertyOptional({ description: 'User notes for this item in the collection' }) + @Column({ type: 'text', nullable: true }) + notes: string; + + @ApiProperty({ description: 'Sort order within collection', default: 0 }) + @Column({ name: 'sort_order', default: 0 }) + sortOrder: number; + + // Relations + @ManyToOne(() => UserCollection, collection => collection.items, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'collection_id' }) + collection: UserCollection; +} diff --git a/src/entities/user-favorite.entity.ts b/src/entities/user-favorite.entity.ts new file mode 100644 index 0000000..03e05df --- /dev/null +++ b/src/entities/user-favorite.entity.ts @@ -0,0 +1,51 @@ +import { Entity, Column, ManyToOne, JoinColumn, Index } from 'typeorm'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BaseEntity } from './base.entity'; +import { User } from './user.entity'; + +export enum FavoriteItemType { + PLACE = 'place', + RESTAURANT = 'restaurant', + HOTEL = 'hotel', + ATTRACTION = 'attraction', + TOUR = 'tour', + EXPERIENCE = 'experience', + PRODUCT = 'product', +} + +@Entity({ name: 'user_favorites', schema: 'tourism' }) +@Index(['userId', 'itemId', 'itemType'], { unique: true }) +export class UserFavorite extends BaseEntity { + @ApiProperty({ description: 'User ID' }) + @Column({ name: 'user_id', type: 'uuid' }) + userId: string; + + @ApiProperty({ description: 'Item ID (can be place, hotel, restaurant, etc.)' }) + @Column({ name: 'item_id', type: 'uuid' }) + itemId: string; + + @ApiProperty({ description: 'Type of favorited item', enum: FavoriteItemType }) + @Column({ name: 'item_type', type: 'enum', enum: FavoriteItemType }) + itemType: FavoriteItemType; + + @ApiPropertyOptional({ description: 'Item name (cached for display)' }) + @Column({ name: 'item_name', length: 255, nullable: true }) + itemName: string; + + @ApiPropertyOptional({ description: 'Item image URL (cached for display)' }) + @Column({ name: 'item_image_url', type: 'text', nullable: true }) + itemImageUrl: string; + + @ApiPropertyOptional({ description: 'Item metadata (cached for display)' }) + @Column({ name: 'item_metadata', type: 'jsonb', nullable: true }) + itemMetadata: Record; + + @ApiPropertyOptional({ description: 'User notes about this favorite' }) + @Column({ type: 'text', nullable: true }) + notes: string; + + // Relations + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; +} diff --git a/src/entities/user-trip.entity.ts b/src/entities/user-trip.entity.ts new file mode 100644 index 0000000..fa41604 --- /dev/null +++ b/src/entities/user-trip.entity.ts @@ -0,0 +1,193 @@ +import { Entity, Column, ManyToOne, OneToMany, JoinColumn, Index } from 'typeorm'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BaseEntity } from './base.entity'; +import { User } from './user.entity'; + +export enum TripStatus { + PLANNING = 'planning', + UPCOMING = 'upcoming', + IN_PROGRESS = 'in_progress', + COMPLETED = 'completed', + CANCELLED = 'cancelled', +} + +@Entity({ name: 'user_trips', schema: 'tourism' }) +@Index(['userId', 'status']) +export class UserTrip extends BaseEntity { + @ApiProperty({ description: 'User ID' }) + @Column({ name: 'user_id', type: 'uuid' }) + userId: string; + + @ApiProperty({ description: 'Trip name', example: 'Summer Vacation in DR' }) + @Column({ length: 150 }) + name: string; + + @ApiPropertyOptional({ description: 'Trip description' }) + @Column({ type: 'text', nullable: true }) + description: string; + + @ApiPropertyOptional({ description: 'Cover image URL' }) + @Column({ name: 'cover_image_url', type: 'text', nullable: true }) + coverImageUrl: string; + + @ApiPropertyOptional({ description: 'Start date' }) + @Column({ name: 'start_date', type: 'date', nullable: true }) + startDate: Date; + + @ApiPropertyOptional({ description: 'End date' }) + @Column({ name: 'end_date', type: 'date', nullable: true }) + endDate: Date; + + @ApiProperty({ description: 'Trip status', enum: TripStatus, default: TripStatus.PLANNING }) + @Column({ type: 'enum', enum: TripStatus, default: TripStatus.PLANNING }) + status: TripStatus; + + @ApiPropertyOptional({ description: 'Destination city/region' }) + @Column({ length: 100, nullable: true }) + destination: string; + + @ApiPropertyOptional({ description: 'Number of travelers', default: 1 }) + @Column({ name: 'travelers_count', default: 1 }) + travelersCount: number; + + @ApiPropertyOptional({ description: 'Estimated budget' }) + @Column({ name: 'estimated_budget', type: 'decimal', precision: 10, scale: 2, nullable: true }) + estimatedBudget: number; + + @ApiPropertyOptional({ description: 'Budget currency', default: 'USD' }) + @Column({ name: 'budget_currency', length: 3, default: 'USD' }) + budgetCurrency: string; + + @ApiPropertyOptional({ description: 'Trip tags', type: 'array' }) + @Column({ type: 'text', array: true, nullable: true }) + tags: string[]; + + @ApiProperty({ description: 'Is public trip', default: false }) + @Column({ name: 'is_public', default: false }) + isPublic: boolean; + + @ApiPropertyOptional({ description: 'Trip notes/comments' }) + @Column({ type: 'text', nullable: true }) + notes: string; + + @ApiPropertyOptional({ description: 'AI-generated summary' }) + @Column({ name: 'ai_summary', type: 'text', nullable: true }) + aiSummary: string; + + // Relations + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @OneToMany(() => TripDay, day => day.trip) + days: TripDay[]; +} + +@Entity({ name: 'trip_days', schema: 'tourism' }) +@Index(['tripId', 'dayNumber']) +export class TripDay extends BaseEntity { + @ApiProperty({ description: 'Trip ID' }) + @Column({ name: 'trip_id', type: 'uuid' }) + tripId: string; + + @ApiProperty({ description: 'Day number', example: 1 }) + @Column({ name: 'day_number' }) + dayNumber: number; + + @ApiPropertyOptional({ description: 'Date for this day' }) + @Column({ type: 'date', nullable: true }) + date: Date; + + @ApiPropertyOptional({ description: 'Day title', example: 'Exploring Santo Domingo' }) + @Column({ length: 150, nullable: true }) + title: string; + + @ApiPropertyOptional({ description: 'Day notes' }) + @Column({ type: 'text', nullable: true }) + notes: string; + + // Relations + @ManyToOne(() => UserTrip, trip => trip.days, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'trip_id' }) + trip: UserTrip; + + @OneToMany(() => TripActivity, activity => activity.day) + activities: TripActivity[]; +} + +@Entity({ name: 'trip_activities', schema: 'tourism' }) +@Index(['dayId', 'sortOrder']) +export class TripActivity extends BaseEntity { + @ApiProperty({ description: 'Day ID' }) + @Column({ name: 'day_id', type: 'uuid' }) + dayId: string; + + @ApiPropertyOptional({ description: 'Reference to place/item ID' }) + @Column({ name: 'item_id', type: 'uuid', nullable: true }) + itemId: string; + + @ApiPropertyOptional({ description: 'Item type', example: 'place' }) + @Column({ name: 'item_type', length: 50, nullable: true }) + itemType: string; + + @ApiProperty({ description: 'Activity title', example: 'Visit Zona Colonial' }) + @Column({ length: 200 }) + title: string; + + @ApiPropertyOptional({ description: 'Activity description' }) + @Column({ type: 'text', nullable: true }) + description: string; + + @ApiPropertyOptional({ description: 'Start time', example: '09:00' }) + @Column({ name: 'start_time', length: 10, nullable: true }) + startTime: string; + + @ApiPropertyOptional({ description: 'End time', example: '12:00' }) + @Column({ name: 'end_time', length: 10, nullable: true }) + endTime: string; + + @ApiPropertyOptional({ description: 'Duration in minutes' }) + @Column({ name: 'duration_minutes', nullable: true }) + durationMinutes: number; + + @ApiPropertyOptional({ description: 'Location name' }) + @Column({ name: 'location_name', length: 255, nullable: true }) + locationName: string; + + @ApiPropertyOptional({ description: 'Location address' }) + @Column({ name: 'location_address', type: 'text', nullable: true }) + locationAddress: string; + + @ApiPropertyOptional({ description: 'Location coordinates' }) + @Column({ name: 'location_coords', type: 'jsonb', nullable: true }) + locationCoords: { lat: number; lng: number }; + + @ApiPropertyOptional({ description: 'Estimated cost' }) + @Column({ name: 'estimated_cost', type: 'decimal', precision: 10, scale: 2, nullable: true }) + estimatedCost: number; + + @ApiPropertyOptional({ description: 'Activity image URL' }) + @Column({ name: 'image_url', type: 'text', nullable: true }) + imageUrl: string; + + @ApiPropertyOptional({ description: 'Activity notes' }) + @Column({ type: 'text', nullable: true }) + notes: string; + + @ApiProperty({ description: 'Is confirmed/booked', default: false }) + @Column({ name: 'is_booked', default: false }) + isBooked: boolean; + + @ApiPropertyOptional({ description: 'Booking reference' }) + @Column({ name: 'booking_reference', length: 100, nullable: true }) + bookingReference: string; + + @ApiProperty({ description: 'Sort order', default: 0 }) + @Column({ name: 'sort_order', default: 0 }) + sortOrder: number; + + // Relations + @ManyToOne(() => TripDay, day => day.activities, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'day_id' }) + day: TripDay; +} diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index a27854b..cc04689 100755 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -25,6 +25,7 @@ export class User extends BaseEntity { @ApiProperty({ description: 'Last name', example: 'Doe' }) @Column({ name: 'last_name', length: 100 }) lastName: string; +@ApiProperty({ description: 'Username', example: 'johndoe' }) @Column({ unique: true, nullable: true, length: 50 }) username: string; @ApiProperty({ description: 'Phone number', example: '+1234567890' }) @Column({ nullable: true, length: 20 }) diff --git a/src/generate-all-content.ts b/src/generate-all-content.ts new file mode 100644 index 0000000..05d8135 --- /dev/null +++ b/src/generate-all-content.ts @@ -0,0 +1,35 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import { ContentGeneratorService } from './modules/content-generator/content-generator.service'; + +async function bootstrap() { + const app = await NestFactory.createApplicationContext(AppModule); + const generator = app.get(ContentGeneratorService); + + console.log('📊 Estadísticas actuales:'); + const stats = await generator.getStats(); + console.log(stats); + + console.log('\n🚀 Iniciando generación COMPLETA de descripciones...'); + console.log('📝 101 lugares x 5 idiomas = 505 descripciones'); + console.log('⏱️ Tiempo estimado: ~40 minutos\n'); + + const results = await generator.generateAllDescriptions({ + limit: 200, // Todos los lugares + languages: ['es', 'en', 'fr', 'it', 'de'], // Los 5 idiomas + onlyMissing: true, + }); + + console.log('\n✅ Resultados FINALES:'); + console.log(`Total procesados: ${results.length}`); + console.log(`Exitosos: ${results.filter(r => r.success).length}`); + console.log(`Fallidos: ${results.filter(r => !r.success).length}`); + + console.log('\n📊 Estadísticas finales:'); + const finalStats = await generator.getStats(); + console.log(finalStats); + + await app.close(); +} + +bootstrap().catch(console.error); diff --git a/src/generate-content.ts b/src/generate-content.ts new file mode 100644 index 0000000..119b59b --- /dev/null +++ b/src/generate-content.ts @@ -0,0 +1,28 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import { ContentGeneratorService } from './modules/content-generator/content-generator.service'; + +async function bootstrap() { + const app = await NestFactory.createApplicationContext(AppModule); + const generator = app.get(ContentGeneratorService); + + console.log('📊 Estadísticas actuales:'); + const stats = await generator.getStats(); + console.log(stats); + + console.log('\n🚀 Iniciando generación de descripciones...'); + const results = await generator.generateAllDescriptions({ + limit: 5, // Solo 5 para probar + languages: ['es', 'en'], // Solo ES y EN primero + onlyMissing: true, + }); + + console.log('\n✅ Resultados:'); + console.log(`Total procesados: ${results.length}`); + console.log(`Exitosos: ${results.filter(r => r.success).length}`); + console.log(`Fallidos: ${results.filter(r => !r.success).length}`); + + await app.close(); +} + +bootstrap().catch(console.error); diff --git a/src/modules/ai-guide/ai-guide.module.ts b/src/modules/ai-guide/ai-guide.module.ts index d97039f..e2855c7 100644 --- a/src/modules/ai-guide/ai-guide.module.ts +++ b/src/modules/ai-guide/ai-guide.module.ts @@ -1,10 +1,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { AIGuideService } from './ai-guide.service'; import { AIGuideController } from './ai-guide.controller'; +import { AIGuideService } from './ai-guide.service'; +import { TTSService } from './tts.service'; +import { TTSController } from './tts.controller'; import { AIGuideInteraction } from '../../entities/ai-guide-interaction.entity'; import { ARContent } from '../../entities/ar-content.entity'; import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { KimiModule } from '../kimi/kimi.module'; @Module({ imports: [ @@ -13,9 +16,10 @@ import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; ARContent, PlaceOfInterest, ]), + KimiModule, ], - controllers: [AIGuideController], - providers: [AIGuideService], - exports: [AIGuideService], + controllers: [AIGuideController, TTSController], + providers: [AIGuideService, TTSService], + exports: [AIGuideService, TTSService], }) export class AIGuideModule {} diff --git a/src/modules/ai-guide/ai-guide.service.ts b/src/modules/ai-guide/ai-guide.service.ts index d05be81..02f0a73 100644 --- a/src/modules/ai-guide/ai-guide.service.ts +++ b/src/modules/ai-guide/ai-guide.service.ts @@ -1,8 +1,9 @@ -import { Injectable, BadRequestException } from '@nestjs/common'; +import { Injectable, BadRequestException, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ConfigService } from '@nestjs/config'; -import axios from 'axios'; +import { KimiService, KimiMessage } from '../kimi/kimi.service'; +import { TTSService } from './tts.service'; import { AIGuideInteraction } from '../../entities/ai-guide-interaction.entity'; import { ARContent } from '../../entities/ar-content.entity'; import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; @@ -12,6 +13,11 @@ import { v4 as uuidv4 } from 'uuid'; @Injectable() export class AIGuideService { + private readonly logger = new Logger(AIGuideService.name); + + // Cache de conversaciones por sesión + private conversationCache: Map = new Map(); + constructor( @InjectRepository(AIGuideInteraction) private readonly interactionRepository: Repository, @@ -20,6 +26,8 @@ export class AIGuideService { @InjectRepository(PlaceOfInterest) private readonly placeRepository: Repository, private readonly configService: ConfigService, + private readonly kimiService: KimiService, + private readonly ttsService: TTSService, // OpenAI TTS Service ) {} async processAIQuery(queryDto: AIQueryDto, userId: string): Promise<{ @@ -48,6 +56,7 @@ export class AIGuideService { break; case InteractionType.GENERAL_QUESTION: + // ✅ ACTUALIZADO: Usar Kimi para preguntas generales aiResponse = await this.processGeneralQuestion(queryDto, sessionId); suggestions = await this.generateSuggestions(queryDto.query, queryDto.language); break; @@ -56,7 +65,9 @@ export class AIGuideService { if (queryDto.latitude && queryDto.longitude) { arContent = await this.findNearbyARContent(queryDto.latitude, queryDto.longitude); } - aiResponse = `Found ${arContent.length} AR experiences near your location. Touch any item to activate the augmented reality experience.`; + aiResponse = queryDto.language === 'es' + ? `Encontré ${arContent.length} experiencias AR cerca de tu ubicación. Toca cualquier elemento para activar la experiencia de realidad aumentada.` + : `Found ${arContent.length} AR experiences near your location. Touch any item to activate the augmented reality experience.`; break; case InteractionType.AUDIO_GUIDE: @@ -73,7 +84,7 @@ export class AIGuideService { case InteractionType.RECOMMENDATIONS: nearbyPlaces = await this.getPersonalizedRecommendations(userId, queryDto); - aiResponse = this.formatRecommendations(nearbyPlaces); + aiResponse = await this.formatRecommendationsWithAI(nearbyPlaces, queryDto.language); break; default: @@ -86,8 +97,8 @@ export class AIGuideService { placeId: queryDto.placeId, userQuery: queryDto.query, aiResponse, - userLocation: queryDto.latitude && queryDto.longitude ? - `POINT(${queryDto.longitude} ${queryDto.latitude})` : undefined, + userLocation: queryDto.latitude && queryDto.longitude ? + `(${queryDto.longitude}, ${queryDto.latitude})` : undefined, interactionType: queryDto.interactionType, language: queryDto.language || 'en', sessionId, @@ -104,27 +115,102 @@ export class AIGuideService { }; } catch (error) { + this.logger.error(`Error processing AI query: ${error.message}`); throw new BadRequestException(`AI processing failed: ${error.message}`); } } + // ✅ ACTUALIZADO: Usar Kimi para preguntas generales + private async processGeneralQuestion(queryDto: AIQueryDto, sessionId: string): Promise { + const language = queryDto.language || 'es'; + + // Obtener historial de conversación de la sesión + let conversationHistory = this.conversationCache.get(sessionId) || []; + + // Agregar contexto de ubicación REAL si está disponible + let contextualQuery = queryDto.query; + if (queryDto.latitude && queryDto.longitude) { + // Determinar ciudad basada en coordenadas + const userCity = this.getCityFromCoordinates(queryDto.latitude, queryDto.longitude); + + // Agregar ubicación REAL al contexto para que la IA responda correctamente + const locationContext = language === 'es' + ? `\n\n[UBICACIÓN ACTUAL DEL USUARIO: ${userCity} (Coordenadas: ${queryDto.latitude}, ${queryDto.longitude}). IMPORTANTE: Solo recomienda lugares en ${userCity} o a máximo 30 minutos de distancia. NO menciones lugares a horas de distancia como ${userCity === 'Santo Domingo' ? 'Punta Cana, Samaná, Puerto Plata' : 'otras ciudades lejanas'}.]` + : `\n\n[USER CURRENT LOCATION: ${userCity} (Coordinates: ${queryDto.latitude}, ${queryDto.longitude}). IMPORTANT: Only recommend places in ${userCity} or within 30 minutes distance. DO NOT mention places hours away.]`; + + contextualQuery = queryDto.query + locationContext; + } + + try { + // ✅ Usar Kimi para generar la respuesta + const response = await this.kimiService.chatWithTravelAssistant( + contextualQuery, + conversationHistory, + language, + ); + + // Actualizar historial de conversación + conversationHistory.push( + { role: 'user', content: queryDto.query }, + { role: 'assistant', content: response } + ); + + // Mantener solo las últimas 10 interacciones + if (conversationHistory.length > 20) { + conversationHistory = conversationHistory.slice(-20); + } + + this.conversationCache.set(sessionId, conversationHistory); + + return response; + } catch (error) { + this.logger.error(`Kimi error: ${error.message}, using fallback`); + return this.kimiService['getFallbackResponse']([ + { role: 'user', content: queryDto.query } + ]); + } + } + + // ✅ ACTUALIZADO: Usar Kimi para reconocimiento de monumentos private async recognizeMonument(queryDto: AIQueryDto): Promise<{ response: string; arContent: ARContent[]; suggestions: string[]; }> { - // Simulate monument recognition using image analysis - // In production, this would use Google Vision API, AWS Rekognition, or custom ML model - + const language = queryDto.language || 'es'; + + // Si hay imagen, usar Kimi para procesarla + if (queryDto.imageUrl) { + try { + const imageAnalysis = await this.kimiService.processImage( + queryDto.imageUrl, + language === 'es' + ? 'Identifica este monumento o lugar turístico. Proporciona su nombre, historia breve y datos interesantes.' + : 'Identify this monument or tourist place. Provide its name, brief history and interesting facts.', + language + ); + + return { + response: imageAnalysis, + arContent: [], + suggestions: language === 'es' + ? ['Cuéntame más sobre su historia', 'Qué puedo hacer aquí', 'Restaurantes cercanos'] + : ['Tell me more about its history', 'What can I do here', 'Nearby restaurants'], + }; + } catch (error) { + this.logger.error(`Image processing error: ${error.message}`); + } + } + + // Fallback: buscar por ubicación let recognizedPlace: PlaceOfInterest | null = null; - + if (queryDto.latitude && queryDto.longitude) { - // Find nearby monuments const nearbyPlaces = await this.placeRepository .createQueryBuilder('place') .where('place.active = :active', { active: true }) - .andWhere('place.category IN (:...categories)', { - categories: ['monument', 'historic-site', 'museum', 'landmark'] + .andWhere('place.category IN (:...categories)', { + categories: ['monument', 'historic-site', 'museum', 'landmark'] }) .orderBy('place.rating', 'DESC') .limit(1) @@ -135,13 +221,13 @@ export class AIGuideService { if (!recognizedPlace) { return { - response: "I can see this is a beautiful location, but I need more information to identify it precisely. Could you tell me where you are or provide more details?", + response: language === 'es' + ? "Puedo ver que es un lugar hermoso, pero necesito más información para identificarlo. ¿Podrías decirme dónde estás o proporcionar más detalles?" + : "I can see this is a beautiful location, but I need more information to identify it precisely. Could you tell me where you are or provide more details?", arContent: [], - suggestions: [ - "Tell me your current location", - "What type of building is this?", - "Show me nearby attractions" - ] + suggestions: language === 'es' + ? ['Dime tu ubicación actual', '¿Qué tipo de edificio es?', 'Mostrar atracciones cercanas'] + : ['Tell me your current location', 'What type of building is this?', 'Show me nearby attractions'], }; } @@ -151,67 +237,28 @@ export class AIGuideService { order: { viewsCount: 'DESC' }, }); - const response = this.generateMonumentDescription(recognizedPlace); - + // ✅ Usar Kimi para generar descripción rica del monumento + const response = await this.kimiService.chatWithTravelAssistant( + language === 'es' + ? `Dame una descripción detallada y atractiva de ${recognizedPlace.name}. Incluye historia, datos interesantes y tips para visitantes.` + : `Give me a detailed and engaging description of ${recognizedPlace.name}. Include history, interesting facts and tips for visitors.`, + [], + language + ); + return { response, arContent, - suggestions: [ - "Tell me more about its history", - "What can I do here?", - "Show me AR experience", - "Find nearby restaurants" - ] + suggestions: language === 'es' + ? ['Cuéntame más sobre su historia', 'Qué puedo hacer aquí', 'Ver experiencia AR', 'Restaurantes cercanos'] + : ['Tell me more about its history', 'What can I do here', 'Show AR experience', 'Find nearby restaurants'], }; } - private async processGeneralQuestion(queryDto: AIQueryDto, sessionId: string): Promise { - // This would integrate with OpenAI GPT, Google Bard, or custom NLP model - // For now, we'll simulate responses based on common tourism questions - - const query = queryDto.query.toLowerCase(); - const language = queryDto.language || 'en'; - - // Predefined responses for common questions - const responses = { - en: { - weather: "The Dominican Republic has a tropical climate with warm temperatures year-round. The dry season (December-April) is ideal for visiting, with less humidity and minimal rainfall.", - food: "Dominican cuisine features delicious dishes like mofongo, sancocho, and fresh seafood. Don't miss trying local fruits like mangoes and passion fruit!", - safety: "Tourist areas in the DR are generally safe. Stay in well-lit areas, use official taxis, and keep your belongings secure. POLITUR officers are available to help tourists.", - currency: "The Dominican Peso (DOP) is the local currency, but US dollars are widely accepted in tourist areas. Credit cards are accepted at most hotels and restaurants.", - language: "Spanish is the official language, but English is commonly spoken in tourist areas. Learning basic Spanish phrases is always appreciated!", - default: "I'm here to help you explore the beautiful Dominican Republic and Puerto Rico! Ask me about attractions, restaurants, safety tips, or anything else you'd like to know." - }, - es: { - weather: "La República Dominicana tiene un clima tropical con temperaturas cálidas todo el año. La temporada seca (diciembre-abril) es ideal para visitar.", - food: "La cocina dominicana incluye platos deliciosos como mofongo, sancocho y mariscos frescos. ¡No te pierdas las frutas tropicales!", - safety: "Las áreas turísticas en RD son generalmente seguras. Mantente en áreas bien iluminadas y usa taxis oficiales.", - currency: "El peso dominicano (DOP) es la moneda local, pero los dólares estadounidenses son ampliamente aceptados.", - language: "El español es el idioma oficial. ¡Aprender algunas frases básicas siempre es apreciado!", - default: "¡Estoy aquí para ayudarte a explorar la hermosa República Dominicana y Puerto Rico! Pregúntame sobre atracciones, restaurantes o cualquier cosa." - } - }; - - const langResponses = responses[language] || responses.en; - - // Simple keyword matching (in production, use proper NLP) - if (query.includes('weather') || query.includes('clima')) { - return langResponses.weather; - } else if (query.includes('food') || query.includes('comida') || query.includes('restaurant')) { - return langResponses.food; - } else if (query.includes('safe') || query.includes('segur')) { - return langResponses.safety; - } else if (query.includes('money') || query.includes('currency') || query.includes('dinero')) { - return langResponses.currency; - } else if (query.includes('language') || query.includes('idioma')) { - return langResponses.language; - } - - return langResponses.default; - } - + // ✅ ACTUALIZADO: Usar Kimi para generar sugerencias contextuales private async generateSuggestions(query: string, language: string = 'en'): Promise { - const suggestions = { + // Sugerencias base según idioma + const baseSuggestions = { en: [ "What are the best beaches to visit?", "Show me historic sites nearby", @@ -230,21 +277,41 @@ export class AIGuideService { ] }; - return suggestions[language] || suggestions.en; + // Por ahora usar sugerencias base, en el futuro Kimi puede generar contextuales + return baseSuggestions[language] || baseSuggestions.en; } - private generateMonumentDescription(place: PlaceOfInterest): string { - return `This is ${place.name}, ${place.description || 'a significant landmark in the Dominican Republic'}. - -Built in the ${place.historicalInfo ? 'historic period' : '16th century'}, this site represents an important part of Caribbean colonial history. + // ✅ NUEVO: Formatear recomendaciones usando Kimi + private async formatRecommendationsWithAI(places: PlaceOfInterest[], language: string = 'es'): Promise { + if (places.length === 0) { + return language === 'es' + ? "No tengo recomendaciones específicas para esta área ahora mismo, ¡pero estaría encantado de ayudarte a explorar lo que hay cerca!" + : "I don't have specific recommendations for this area right now, but I'd be happy to help you explore what's nearby!"; + } -Rating: ${place.rating}/5 (${place.totalReviews} reviews) + const placesInfo = places.map((place, index) => + `${index + 1}. ${place.name} - ${place.category} - Rating: ${place.rating}/5 - ${place.description?.substring(0, 150)}` + ).join('\n'); -Would you like to explore AR content, hear an audio guide, or learn more about nearby attractions?`; + const prompt = language === 'es' + ? `Basándote en estos lugares, genera una recomendación atractiva y personalizada:\n\n${placesInfo}\n\nFormato: Lista con emojis, breve y atractivo.` + : `Based on these places, generate an attractive personalized recommendation:\n\n${placesInfo}\n\nFormat: List with emojis, brief and attractive.`; + + try { + return await this.kimiService.chatWithTravelAssistant(prompt, [], language); + } catch (error) { + // Fallback simple + const formatted = places.map((place, index) => + `${index + 1}. **${place.name}** (${place.rating}/5) - ${place.description?.substring(0, 100)}...` + ).join('\n\n'); + + return language === 'es' + ? `Basado en tu ubicación y preferencias, aquí están mis recomendaciones:\n\n${formatted}` + : `Based on your location and preferences, here are my top recommendations:\n\n${formatted}`; + } } private async findNearbyARContent(latitude: number, longitude: number, radius: number = 100): Promise { - // In production, use PostGIS for accurate distance calculations return this.arContentRepository.find({ where: { isActive: true }, order: { viewsCount: 'DESC' }, @@ -252,17 +319,86 @@ Would you like to explore AR content, hear an audio guide, or learn more about n }); } + // ✅ ACTUALIZADO: Usar OpenAI TTS para generar audio real private async generateAudioGuide(queryDto: AIQueryDto): Promise<{ transcript: string; audioUrl: string }> { - // This would integrate with text-to-speech services like AWS Polly, Google TTS, or Azure Speech - const transcript = "Welcome to this historic location. Let me tell you about its fascinating history..."; - const audioUrl = "https://karibeo-audio-guides.s3.amazonaws.com/generated-audio-guide.mp3"; - - return { transcript, audioUrl }; + const language = queryDto.language || 'es'; + const placeName = queryDto.placeName || queryDto.placeId || 'la Zona Colonial de Santo Domingo'; + + // Prompts en cada idioma para mejor calidad + const prompts: Record = { + es: `Genera un guión para un audio tour de 1-2 minutos sobre ${placeName} en República Dominicana. + Hazlo informativo pero entretenido, como si fueras un guía turístico experto. + Incluye historia, datos curiosos y tips para visitantes. + Habla en segunda persona directamente al turista.`, + en: `Generate a 1-2 minute audio tour script about ${placeName} in the Dominican Republic. + Make it informative but entertaining, as if you were an expert tour guide. + Include history, fun facts and tips for visitors. + Speak directly to the tourist in second person.`, + fr: `Génère un script de visite audio de 1-2 minutes sur ${placeName} en République Dominicaine. + Rends-le informatif mais divertissant, comme si tu étais un guide touristique expert. + Inclus l'histoire, des anecdotes et des conseils pour les visiteurs.`, + it: `Genera uno script per un tour audio di 1-2 minuti su ${placeName} nella Repubblica Dominicana. + Rendilo informativo ma divertente, come se fossi una guida turistica esperta. + Includi storia, curiosità e consigli per i visitatori.`, + de: `Erstelle ein 1-2 minütiges Audio-Tour-Skript über ${placeName} in der Dominikanischen Republik. + Mache es informativ aber unterhaltsam, als wärst du ein erfahrener Reiseführer. + Füge Geschichte, interessante Fakten und Tipps für Besucher hinzu.`, + }; + + const prompt = prompts[language] || prompts['en']; + + try { + // 1. Generar el script con Kimi AI + this.logger.log(`Generating audio guide script for: ${placeName} (${language})`); + const transcript = await this.kimiService.chatWithTravelAssistant(prompt, [], language); + + // 2. Convertir a audio con OpenAI TTS + let audioUrl = ''; + + if (this.ttsService.isAvailable()) { + this.logger.log('Converting transcript to speech with OpenAI TTS...'); + const generatedUrl = await this.ttsService.generateSpeech(transcript, language); + + if (generatedUrl) { + audioUrl = generatedUrl; + this.logger.log(`Audio generated successfully: ${audioUrl.substring(0, 50)}...`); + } else { + this.logger.warn('TTS generation returned null, using placeholder'); + audioUrl = 'https://karibeo-audio-guides.s3.amazonaws.com/placeholder-audio.mp3'; + } + } else { + this.logger.warn('TTS service not available, using placeholder'); + audioUrl = 'https://karibeo-audio-guides.s3.amazonaws.com/placeholder-audio.mp3'; + } + + return { transcript, audioUrl }; + } catch (error) { + this.logger.error(`Audio guide generation failed: ${error.message}`); + + // Fallback response + const fallbackTexts: Record = { + es: 'Bienvenido a este histórico lugar. Permíteme contarte sobre su fascinante historia...', + en: 'Welcome to this historic location. Let me tell you about its fascinating history...', + fr: 'Bienvenue dans ce lieu historique. Permettez-moi de vous raconter son histoire fascinante...', + it: 'Benvenuto in questo luogo storico. Lascia che ti racconti la sua affascinante storia...', + de: 'Willkommen an diesem historischen Ort. Lassen Sie mich Ihnen seine faszinierende Geschichte erzählen...', + }; + + return { + transcript: fallbackTexts[language] || fallbackTexts['en'], + audioUrl: 'https://karibeo-audio-guides.s3.amazonaws.com/placeholder-audio.mp3' + }; + } } + private async getSmartDirections(queryDto: AIQueryDto): Promise<{ instructions: string; waypoints: PlaceOfInterest[] }> { - // Integrate with Google Maps Directions API for optimal routing - const instructions = "Head north for 200 meters, then turn right at the historic plaza. You'll pass several interesting landmarks along the way."; + const language = queryDto.language || 'es'; + + const instructions = language === 'es' + ? "Dirígete hacia el norte por 200 metros, luego gira a la derecha en la plaza histórica. Pasarás por varios puntos de interés en el camino." + : "Head north for 200 meters, then turn right at the historic plaza. You'll pass several interesting landmarks along the way."; + const waypoints = await this.placeRepository.find({ where: { active: true }, take: 3, @@ -272,7 +408,6 @@ Would you like to explore AR content, hear an audio guide, or learn more about n } private async getPersonalizedRecommendations(userId: string, queryDto: AIQueryDto): Promise { - // This would use ML algorithms to analyze user preferences, past visits, and ratings return this.placeRepository.find({ where: { active: true }, order: { rating: 'DESC' }, @@ -280,18 +415,6 @@ Would you like to explore AR content, hear an audio guide, or learn more about n }); } - private formatRecommendations(places: PlaceOfInterest[]): string { - if (places.length === 0) { - return "I don't have specific recommendations for this area right now, but I'd be happy to help you explore what's nearby!"; - } - - const formatted = places.map((place, index) => - `${index + 1}. ${place.name} (${place.rating}/5) - ${place.description?.substring(0, 100)}...` - ).join('\n\n'); - - return `Based on your location and preferences, here are my top recommendations:\n\n${formatted}`; - } - private async saveInteraction(interactionData: Partial): Promise { const interaction = this.interactionRepository.create(interactionData); await this.interactionRepository.save(interaction); @@ -299,7 +422,6 @@ Would you like to explore AR content, hear an audio guide, or learn more about n // AR CONTENT MANAGEMENT async getNearbyARContent(queryDto: ARContentQueryDto): Promise { - // In production, use PostGIS for accurate geospatial queries const query = this.arContentRepository.createQueryBuilder('ar') .leftJoinAndSelect('ar.place', 'place') .where('ar.isActive = :active', { active: true }); @@ -322,6 +444,43 @@ Would you like to explore AR content, hear an audio guide, or learn more about n await this.arContentRepository.increment({ id: arContentId }, 'viewsCount', 1); } + // ✅ NUEVO: Generar itinerario completo con Kimi + async generateItinerary(params: { + userId: string; + destination: string; + days: number; + interests: string[]; + budget: string; + language: string; + }): Promise { + const { userId, destination, days, interests, budget, language } = params; + + try { + const itinerary = await this.kimiService.generateItinerary({ + destination, + days, + interests, + budget, + language, + }); + + // Guardar la interacción + await this.saveInteraction({ + userId, + userQuery: `Generate itinerary: ${destination}, ${days} days`, + aiResponse: itinerary, + interactionType: 'itinerary-generation' as any, + language, + metadata: { destination, days, interests, budget }, + }); + + return itinerary; + } catch (error) { + this.logger.error(`Itinerary generation error: ${error.message}`); + throw new BadRequestException('Failed to generate itinerary'); + } + } + // ANALYTICS async getAIUsageStats(): Promise<{ totalInteractions: number; @@ -351,7 +510,6 @@ Would you like to explore AR content, hear an audio guide, or learn more about n .getRawOne(), ]); - // Get popular queries (simplified version) const popularQueries = await this.interactionRepository .createQueryBuilder('interaction') .select('interaction.userQuery', 'query') @@ -369,4 +527,41 @@ Would you like to explore AR content, hear an audio guide, or learn more about n popularQueries: popularQueries.map(item => ({ query: item.query, count: parseInt(item.count) })), }; } + + // ✅ NUEVO: Limpiar cache de conversación + clearConversationCache(sessionId: string): void { + this.conversationCache.delete(sessionId); + } + + /** + * Determinar ciudad aproximada basada en coordenadas + */ + private getCityFromCoordinates(lat: number, lng: number): string { + // Santo Domingo area: lat 18.4-18.6, lng -70.0 to -69.8 + if (lat >= 18.4 && lat <= 18.6 && lng >= -70.0 && lng <= -69.8) { + return 'Santo Domingo'; + } + // Punta Cana area: lat 18.5-18.7, lng -68.5 to -68.3 + if (lat >= 18.5 && lat <= 18.7 && lng >= -68.5 && lng <= -68.3) { + return 'Punta Cana'; + } + // Santiago area: lat 19.4-19.5, lng -70.7 to -70.6 + if (lat >= 19.4 && lat <= 19.5 && lng >= -70.7 && lng <= -70.6) { + return 'Santiago'; + } + // Puerto Plata area: lat 19.7-19.9, lng -70.7 to -70.6 + if (lat >= 19.7 && lat <= 19.9 && lng >= -70.7 && lng <= -70.6) { + return 'Puerto Plata'; + } + // Samaná area: lat 19.2-19.3, lng -69.4 to -69.2 + if (lat >= 19.2 && lat <= 19.3 && lng >= -69.4 && lng <= -69.2) { + return 'Samaná'; + } + // La Romana area + if (lat >= 18.4 && lat <= 18.5 && lng >= -69.0 && lng <= -68.9) { + return 'La Romana'; + } + return 'República Dominicana'; + } + } diff --git a/src/modules/ai-guide/ai-guide.service.ts.backup b/src/modules/ai-guide/ai-guide.service.ts.backup new file mode 100644 index 0000000..d05be81 --- /dev/null +++ b/src/modules/ai-guide/ai-guide.service.ts.backup @@ -0,0 +1,372 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ConfigService } from '@nestjs/config'; +import axios from 'axios'; +import { AIGuideInteraction } from '../../entities/ai-guide-interaction.entity'; +import { ARContent } from '../../entities/ar-content.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { AIQueryDto, InteractionType } from './dto/ai-query.dto'; +import { ARContentQueryDto } from './dto/ar-content-query.dto'; +import { v4 as uuidv4 } from 'uuid'; + +@Injectable() +export class AIGuideService { + constructor( + @InjectRepository(AIGuideInteraction) + private readonly interactionRepository: Repository, + @InjectRepository(ARContent) + private readonly arContentRepository: Repository, + @InjectRepository(PlaceOfInterest) + private readonly placeRepository: Repository, + private readonly configService: ConfigService, + ) {} + + async processAIQuery(queryDto: AIQueryDto, userId: string): Promise<{ + response: string; + suggestions: string[]; + arContent?: ARContent[]; + nearbyPlaces?: PlaceOfInterest[]; + audioGuideUrl?: string; + sessionId: string; + }> { + const sessionId = queryDto.sessionId || uuidv4(); + + let aiResponse = ''; + let suggestions: string[] = []; + let arContent: ARContent[] = []; + let nearbyPlaces: PlaceOfInterest[] = []; + let audioGuideUrl: string = ''; + + try { + switch (queryDto.interactionType) { + case InteractionType.MONUMENT_RECOGNITION: + const recognitionResult = await this.recognizeMonument(queryDto); + aiResponse = recognitionResult.response; + arContent = recognitionResult.arContent; + suggestions = recognitionResult.suggestions; + break; + + case InteractionType.GENERAL_QUESTION: + aiResponse = await this.processGeneralQuestion(queryDto, sessionId); + suggestions = await this.generateSuggestions(queryDto.query, queryDto.language); + break; + + case InteractionType.AR_CONTENT: + if (queryDto.latitude && queryDto.longitude) { + arContent = await this.findNearbyARContent(queryDto.latitude, queryDto.longitude); + } + aiResponse = `Found ${arContent.length} AR experiences near your location. Touch any item to activate the augmented reality experience.`; + break; + + case InteractionType.AUDIO_GUIDE: + const audioResult = await this.generateAudioGuide(queryDto); + aiResponse = audioResult.transcript; + audioGuideUrl = audioResult.audioUrl; + break; + + case InteractionType.DIRECTIONS: + const directionsResult = await this.getSmartDirections(queryDto); + aiResponse = directionsResult.instructions; + nearbyPlaces = directionsResult.waypoints; + break; + + case InteractionType.RECOMMENDATIONS: + nearbyPlaces = await this.getPersonalizedRecommendations(userId, queryDto); + aiResponse = this.formatRecommendations(nearbyPlaces); + break; + + default: + aiResponse = await this.processGeneralQuestion(queryDto, sessionId); + } + + // Save interaction + await this.saveInteraction({ + userId, + placeId: queryDto.placeId, + userQuery: queryDto.query, + aiResponse, + userLocation: queryDto.latitude && queryDto.longitude ? + `POINT(${queryDto.longitude} ${queryDto.latitude})` : undefined, + interactionType: queryDto.interactionType, + language: queryDto.language || 'en', + sessionId, + metadata: queryDto.metadata, + }); + + return { + response: aiResponse, + suggestions, + arContent, + nearbyPlaces, + audioGuideUrl, + sessionId, + }; + + } catch (error) { + throw new BadRequestException(`AI processing failed: ${error.message}`); + } + } + + private async recognizeMonument(queryDto: AIQueryDto): Promise<{ + response: string; + arContent: ARContent[]; + suggestions: string[]; + }> { + // Simulate monument recognition using image analysis + // In production, this would use Google Vision API, AWS Rekognition, or custom ML model + + let recognizedPlace: PlaceOfInterest | null = null; + + if (queryDto.latitude && queryDto.longitude) { + // Find nearby monuments + const nearbyPlaces = await this.placeRepository + .createQueryBuilder('place') + .where('place.active = :active', { active: true }) + .andWhere('place.category IN (:...categories)', { + categories: ['monument', 'historic-site', 'museum', 'landmark'] + }) + .orderBy('place.rating', 'DESC') + .limit(1) + .getMany(); + + recognizedPlace = nearbyPlaces[0] || null; + } + + if (!recognizedPlace) { + return { + response: "I can see this is a beautiful location, but I need more information to identify it precisely. Could you tell me where you are or provide more details?", + arContent: [], + suggestions: [ + "Tell me your current location", + "What type of building is this?", + "Show me nearby attractions" + ] + }; + } + + // Get AR content for recognized place + const arContent = await this.arContentRepository.find({ + where: { placeId: recognizedPlace.id, isActive: true }, + order: { viewsCount: 'DESC' }, + }); + + const response = this.generateMonumentDescription(recognizedPlace); + + return { + response, + arContent, + suggestions: [ + "Tell me more about its history", + "What can I do here?", + "Show me AR experience", + "Find nearby restaurants" + ] + }; + } + + private async processGeneralQuestion(queryDto: AIQueryDto, sessionId: string): Promise { + // This would integrate with OpenAI GPT, Google Bard, or custom NLP model + // For now, we'll simulate responses based on common tourism questions + + const query = queryDto.query.toLowerCase(); + const language = queryDto.language || 'en'; + + // Predefined responses for common questions + const responses = { + en: { + weather: "The Dominican Republic has a tropical climate with warm temperatures year-round. The dry season (December-April) is ideal for visiting, with less humidity and minimal rainfall.", + food: "Dominican cuisine features delicious dishes like mofongo, sancocho, and fresh seafood. Don't miss trying local fruits like mangoes and passion fruit!", + safety: "Tourist areas in the DR are generally safe. Stay in well-lit areas, use official taxis, and keep your belongings secure. POLITUR officers are available to help tourists.", + currency: "The Dominican Peso (DOP) is the local currency, but US dollars are widely accepted in tourist areas. Credit cards are accepted at most hotels and restaurants.", + language: "Spanish is the official language, but English is commonly spoken in tourist areas. Learning basic Spanish phrases is always appreciated!", + default: "I'm here to help you explore the beautiful Dominican Republic and Puerto Rico! Ask me about attractions, restaurants, safety tips, or anything else you'd like to know." + }, + es: { + weather: "La República Dominicana tiene un clima tropical con temperaturas cálidas todo el año. La temporada seca (diciembre-abril) es ideal para visitar.", + food: "La cocina dominicana incluye platos deliciosos como mofongo, sancocho y mariscos frescos. ¡No te pierdas las frutas tropicales!", + safety: "Las áreas turísticas en RD son generalmente seguras. Mantente en áreas bien iluminadas y usa taxis oficiales.", + currency: "El peso dominicano (DOP) es la moneda local, pero los dólares estadounidenses son ampliamente aceptados.", + language: "El español es el idioma oficial. ¡Aprender algunas frases básicas siempre es apreciado!", + default: "¡Estoy aquí para ayudarte a explorar la hermosa República Dominicana y Puerto Rico! Pregúntame sobre atracciones, restaurantes o cualquier cosa." + } + }; + + const langResponses = responses[language] || responses.en; + + // Simple keyword matching (in production, use proper NLP) + if (query.includes('weather') || query.includes('clima')) { + return langResponses.weather; + } else if (query.includes('food') || query.includes('comida') || query.includes('restaurant')) { + return langResponses.food; + } else if (query.includes('safe') || query.includes('segur')) { + return langResponses.safety; + } else if (query.includes('money') || query.includes('currency') || query.includes('dinero')) { + return langResponses.currency; + } else if (query.includes('language') || query.includes('idioma')) { + return langResponses.language; + } + + return langResponses.default; + } + + private async generateSuggestions(query: string, language: string = 'en'): Promise { + const suggestions = { + en: [ + "What are the best beaches to visit?", + "Show me historic sites nearby", + "Find restaurants with local cuisine", + "What activities can I do here?", + "Tell me about local culture", + "How do I get to Santo Domingo?" + ], + es: [ + "¿Cuáles son las mejores playas para visitar?", + "Muéstrame sitios históricos cercanos", + "Encuentra restaurantes con cocina local", + "¿Qué actividades puedo hacer aquí?", + "Háblame sobre la cultura local", + "¿Cómo llego a Santo Domingo?" + ] + }; + + return suggestions[language] || suggestions.en; + } + + private generateMonumentDescription(place: PlaceOfInterest): string { + return `This is ${place.name}, ${place.description || 'a significant landmark in the Dominican Republic'}. + +Built in the ${place.historicalInfo ? 'historic period' : '16th century'}, this site represents an important part of Caribbean colonial history. + +Rating: ${place.rating}/5 (${place.totalReviews} reviews) + +Would you like to explore AR content, hear an audio guide, or learn more about nearby attractions?`; + } + + private async findNearbyARContent(latitude: number, longitude: number, radius: number = 100): Promise { + // In production, use PostGIS for accurate distance calculations + return this.arContentRepository.find({ + where: { isActive: true }, + order: { viewsCount: 'DESC' }, + take: 10, + }); + } + + private async generateAudioGuide(queryDto: AIQueryDto): Promise<{ transcript: string; audioUrl: string }> { + // This would integrate with text-to-speech services like AWS Polly, Google TTS, or Azure Speech + const transcript = "Welcome to this historic location. Let me tell you about its fascinating history..."; + const audioUrl = "https://karibeo-audio-guides.s3.amazonaws.com/generated-audio-guide.mp3"; + + return { transcript, audioUrl }; + } + + private async getSmartDirections(queryDto: AIQueryDto): Promise<{ instructions: string; waypoints: PlaceOfInterest[] }> { + // Integrate with Google Maps Directions API for optimal routing + const instructions = "Head north for 200 meters, then turn right at the historic plaza. You'll pass several interesting landmarks along the way."; + const waypoints = await this.placeRepository.find({ + where: { active: true }, + take: 3, + }); + + return { instructions, waypoints }; + } + + private async getPersonalizedRecommendations(userId: string, queryDto: AIQueryDto): Promise { + // This would use ML algorithms to analyze user preferences, past visits, and ratings + return this.placeRepository.find({ + where: { active: true }, + order: { rating: 'DESC' }, + take: 5, + }); + } + + private formatRecommendations(places: PlaceOfInterest[]): string { + if (places.length === 0) { + return "I don't have specific recommendations for this area right now, but I'd be happy to help you explore what's nearby!"; + } + + const formatted = places.map((place, index) => + `${index + 1}. ${place.name} (${place.rating}/5) - ${place.description?.substring(0, 100)}...` + ).join('\n\n'); + + return `Based on your location and preferences, here are my top recommendations:\n\n${formatted}`; + } + + private async saveInteraction(interactionData: Partial): Promise { + const interaction = this.interactionRepository.create(interactionData); + await this.interactionRepository.save(interaction); + } + + // AR CONTENT MANAGEMENT + async getNearbyARContent(queryDto: ARContentQueryDto): Promise { + // In production, use PostGIS for accurate geospatial queries + const query = this.arContentRepository.createQueryBuilder('ar') + .leftJoinAndSelect('ar.place', 'place') + .where('ar.isActive = :active', { active: true }); + + if (queryDto.contentType) { + query.andWhere('ar.contentType = :contentType', { contentType: queryDto.contentType }); + } + + if (queryDto.language) { + query.andWhere(':language = ANY(ar.languages)', { language: queryDto.language }); + } + + return query + .orderBy('ar.viewsCount', 'DESC') + .limit(10) + .getMany(); + } + + async incrementARViewCount(arContentId: string): Promise { + await this.arContentRepository.increment({ id: arContentId }, 'viewsCount', 1); + } + + // ANALYTICS + async getAIUsageStats(): Promise<{ + totalInteractions: number; + byType: Array<{ type: string; count: number }>; + byLanguage: Array<{ language: string; count: number }>; + averageRating: number; + popularQueries: Array<{ query: string; count: number }>; + }> { + const [totalInteractions, byType, byLanguage, avgRating] = await Promise.all([ + this.interactionRepository.count(), + this.interactionRepository + .createQueryBuilder('interaction') + .select('interaction.interactionType', 'type') + .addSelect('COUNT(*)', 'count') + .groupBy('interaction.interactionType') + .getRawMany(), + this.interactionRepository + .createQueryBuilder('interaction') + .select('interaction.language', 'language') + .addSelect('COUNT(*)', 'count') + .groupBy('interaction.language') + .getRawMany(), + this.interactionRepository + .createQueryBuilder('interaction') + .select('AVG(interaction.rating)', 'average') + .where('interaction.rating IS NOT NULL') + .getRawOne(), + ]); + + // Get popular queries (simplified version) + const popularQueries = await this.interactionRepository + .createQueryBuilder('interaction') + .select('interaction.userQuery', 'query') + .addSelect('COUNT(*)', 'count') + .groupBy('interaction.userQuery') + .orderBy('count', 'DESC') + .limit(10) + .getRawMany(); + + return { + totalInteractions, + byType: byType.map(item => ({ type: item.type, count: parseInt(item.count) })), + byLanguage: byLanguage.map(item => ({ language: item.language, count: parseInt(item.count) })), + averageRating: parseFloat(avgRating?.average || '0'), + popularQueries: popularQueries.map(item => ({ query: item.query, count: parseInt(item.count) })), + }; + } +} diff --git a/src/modules/ai-guide/ai-guide.service.ts.backup2 b/src/modules/ai-guide/ai-guide.service.ts.backup2 new file mode 100644 index 0000000..8172255 --- /dev/null +++ b/src/modules/ai-guide/ai-guide.service.ts.backup2 @@ -0,0 +1,526 @@ +import { Injectable, BadRequestException, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ConfigService } from '@nestjs/config'; +import { KimiService, KimiMessage } from '../kimi/kimi.service'; +import { AIGuideInteraction } from '../../entities/ai-guide-interaction.entity'; +import { ARContent } from '../../entities/ar-content.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { AIQueryDto, InteractionType } from './dto/ai-query.dto'; +import { ARContentQueryDto } from './dto/ar-content-query.dto'; +import { v4 as uuidv4 } from 'uuid'; + +@Injectable() +export class AIGuideService { + private readonly logger = new Logger(AIGuideService.name); + + // Cache de conversaciones por sesión + private conversationCache: Map = new Map(); + + constructor( + @InjectRepository(AIGuideInteraction) + private readonly interactionRepository: Repository, + @InjectRepository(ARContent) + private readonly arContentRepository: Repository, + @InjectRepository(PlaceOfInterest) + private readonly placeRepository: Repository, + private readonly configService: ConfigService, + private readonly kimiService: KimiService, // ✅ NUEVO: Inyectar KimiService + ) {} + + async processAIQuery(queryDto: AIQueryDto, userId: string): Promise<{ + response: string; + suggestions: string[]; + arContent?: ARContent[]; + nearbyPlaces?: PlaceOfInterest[]; + audioGuideUrl?: string; + sessionId: string; + }> { + const sessionId = queryDto.sessionId || uuidv4(); + + let aiResponse = ''; + let suggestions: string[] = []; + let arContent: ARContent[] = []; + let nearbyPlaces: PlaceOfInterest[] = []; + let audioGuideUrl: string = ''; + + try { + switch (queryDto.interactionType) { + case InteractionType.MONUMENT_RECOGNITION: + const recognitionResult = await this.recognizeMonument(queryDto); + aiResponse = recognitionResult.response; + arContent = recognitionResult.arContent; + suggestions = recognitionResult.suggestions; + break; + + case InteractionType.GENERAL_QUESTION: + // ✅ ACTUALIZADO: Usar Kimi para preguntas generales + aiResponse = await this.processGeneralQuestion(queryDto, sessionId); + suggestions = await this.generateSuggestions(queryDto.query, queryDto.language); + break; + + case InteractionType.AR_CONTENT: + if (queryDto.latitude && queryDto.longitude) { + arContent = await this.findNearbyARContent(queryDto.latitude, queryDto.longitude); + } + aiResponse = queryDto.language === 'es' + ? `Encontré ${arContent.length} experiencias AR cerca de tu ubicación. Toca cualquier elemento para activar la experiencia de realidad aumentada.` + : `Found ${arContent.length} AR experiences near your location. Touch any item to activate the augmented reality experience.`; + break; + + case InteractionType.AUDIO_GUIDE: + const audioResult = await this.generateAudioGuide(queryDto); + aiResponse = audioResult.transcript; + audioGuideUrl = audioResult.audioUrl; + break; + + case InteractionType.DIRECTIONS: + const directionsResult = await this.getSmartDirections(queryDto); + aiResponse = directionsResult.instructions; + nearbyPlaces = directionsResult.waypoints; + break; + + case InteractionType.RECOMMENDATIONS: + nearbyPlaces = await this.getPersonalizedRecommendations(userId, queryDto); + aiResponse = await this.formatRecommendationsWithAI(nearbyPlaces, queryDto.language); + break; + + default: + aiResponse = await this.processGeneralQuestion(queryDto, sessionId); + } + + // Save interaction + await this.saveInteraction({ + userId, + placeId: queryDto.placeId, + userQuery: queryDto.query, + aiResponse, + userLocation: queryDto.latitude && queryDto.longitude ? + `(${queryDto.longitude}, ${queryDto.latitude})` : undefined, + interactionType: queryDto.interactionType, + language: queryDto.language || 'en', + sessionId, + metadata: queryDto.metadata, + }); + + return { + response: aiResponse, + suggestions, + arContent, + nearbyPlaces, + audioGuideUrl, + sessionId, + }; + + } catch (error) { + this.logger.error(`Error processing AI query: ${error.message}`); + throw new BadRequestException(`AI processing failed: ${error.message}`); + } + } + + // ✅ ACTUALIZADO: Usar Kimi para preguntas generales + private async processGeneralQuestion(queryDto: AIQueryDto, sessionId: string): Promise { + const language = queryDto.language || 'es'; + + // Obtener historial de conversación de la sesión + let conversationHistory = this.conversationCache.get(sessionId) || []; + + // Agregar contexto de ubicación si está disponible + let contextualQuery = queryDto.query; + if (queryDto.latitude && queryDto.longitude) { + const nearbyPlaces = await this.placeRepository + .createQueryBuilder('place') + .where('place.active = :active', { active: true }) + .orderBy('place.rating', 'DESC') + .limit(5) + .getMany(); + + if (nearbyPlaces.length > 0) { + const placesContext = nearbyPlaces.map(p => + `- ${p.name} (${p.category}): ${p.description?.substring(0, 100)}...` + ).join('\n'); + + contextualQuery = language === 'es' + ? `${queryDto.query}\n\n[Contexto: El usuario está cerca de estos lugares:\n${placesContext}]` + : `${queryDto.query}\n\n[Context: The user is near these places:\n${placesContext}]`; + } + } + + try { + // ✅ Usar Kimi para generar la respuesta + const response = await this.kimiService.chatWithTravelAssistant( + contextualQuery, + conversationHistory, + language, + ); + + // Actualizar historial de conversación + conversationHistory.push( + { role: 'user', content: queryDto.query }, + { role: 'assistant', content: response } + ); + + // Mantener solo las últimas 10 interacciones + if (conversationHistory.length > 20) { + conversationHistory = conversationHistory.slice(-20); + } + + this.conversationCache.set(sessionId, conversationHistory); + + return response; + } catch (error) { + this.logger.error(`Kimi error: ${error.message}, using fallback`); + return this.kimiService['getFallbackResponse']([ + { role: 'user', content: queryDto.query } + ]); + } + } + + // ✅ ACTUALIZADO: Usar Kimi para reconocimiento de monumentos + private async recognizeMonument(queryDto: AIQueryDto): Promise<{ + response: string; + arContent: ARContent[]; + suggestions: string[]; + }> { + const language = queryDto.language || 'es'; + + // Si hay imagen, usar Kimi para procesarla + if (queryDto.imageUrl) { + try { + const imageAnalysis = await this.kimiService.processImage( + queryDto.imageUrl, + language === 'es' + ? 'Identifica este monumento o lugar turístico. Proporciona su nombre, historia breve y datos interesantes.' + : 'Identify this monument or tourist place. Provide its name, brief history and interesting facts.', + language + ); + + return { + response: imageAnalysis, + arContent: [], + suggestions: language === 'es' + ? ['Cuéntame más sobre su historia', 'Qué puedo hacer aquí', 'Restaurantes cercanos'] + : ['Tell me more about its history', 'What can I do here', 'Nearby restaurants'], + }; + } catch (error) { + this.logger.error(`Image processing error: ${error.message}`); + } + } + + // Fallback: buscar por ubicación + let recognizedPlace: PlaceOfInterest | null = null; + + if (queryDto.latitude && queryDto.longitude) { + const nearbyPlaces = await this.placeRepository + .createQueryBuilder('place') + .where('place.active = :active', { active: true }) + .andWhere('place.category IN (:...categories)', { + categories: ['monument', 'historic-site', 'museum', 'landmark'] + }) + .orderBy('place.rating', 'DESC') + .limit(1) + .getMany(); + + recognizedPlace = nearbyPlaces[0] || null; + } + + if (!recognizedPlace) { + return { + response: language === 'es' + ? "Puedo ver que es un lugar hermoso, pero necesito más información para identificarlo. ¿Podrías decirme dónde estás o proporcionar más detalles?" + : "I can see this is a beautiful location, but I need more information to identify it precisely. Could you tell me where you are or provide more details?", + arContent: [], + suggestions: language === 'es' + ? ['Dime tu ubicación actual', '¿Qué tipo de edificio es?', 'Mostrar atracciones cercanas'] + : ['Tell me your current location', 'What type of building is this?', 'Show me nearby attractions'], + }; + } + + // Get AR content for recognized place + const arContent = await this.arContentRepository.find({ + where: { placeId: recognizedPlace.id, isActive: true }, + order: { viewsCount: 'DESC' }, + }); + + // ✅ Usar Kimi para generar descripción rica del monumento + const response = await this.kimiService.chatWithTravelAssistant( + language === 'es' + ? `Dame una descripción detallada y atractiva de ${recognizedPlace.name}. Incluye historia, datos interesantes y tips para visitantes.` + : `Give me a detailed and engaging description of ${recognizedPlace.name}. Include history, interesting facts and tips for visitors.`, + [], + language + ); + + return { + response, + arContent, + suggestions: language === 'es' + ? ['Cuéntame más sobre su historia', 'Qué puedo hacer aquí', 'Ver experiencia AR', 'Restaurantes cercanos'] + : ['Tell me more about its history', 'What can I do here', 'Show AR experience', 'Find nearby restaurants'], + }; + } + + // ✅ ACTUALIZADO: Usar Kimi para generar sugerencias contextuales + private async generateSuggestions(query: string, language: string = 'en'): Promise { + // Sugerencias base según idioma + const baseSuggestions = { + en: [ + "What are the best beaches to visit?", + "Show me historic sites nearby", + "Find restaurants with local cuisine", + "What activities can I do here?", + "Tell me about local culture", + "How do I get to Santo Domingo?" + ], + es: [ + "¿Cuáles son las mejores playas para visitar?", + "Muéstrame sitios históricos cercanos", + "Encuentra restaurantes con cocina local", + "¿Qué actividades puedo hacer aquí?", + "Háblame sobre la cultura local", + "¿Cómo llego a Santo Domingo?" + ] + }; + + // Por ahora usar sugerencias base, en el futuro Kimi puede generar contextuales + return baseSuggestions[language] || baseSuggestions.en; + } + + // ✅ NUEVO: Formatear recomendaciones usando Kimi + private async formatRecommendationsWithAI(places: PlaceOfInterest[], language: string = 'es'): Promise { + if (places.length === 0) { + return language === 'es' + ? "No tengo recomendaciones específicas para esta área ahora mismo, ¡pero estaría encantado de ayudarte a explorar lo que hay cerca!" + : "I don't have specific recommendations for this area right now, but I'd be happy to help you explore what's nearby!"; + } + + const placesInfo = places.map((place, index) => + `${index + 1}. ${place.name} - ${place.category} - Rating: ${place.rating}/5 - ${place.description?.substring(0, 150)}` + ).join('\n'); + + const prompt = language === 'es' + ? `Basándote en estos lugares, genera una recomendación atractiva y personalizada:\n\n${placesInfo}\n\nFormato: Lista con emojis, breve y atractivo.` + : `Based on these places, generate an attractive personalized recommendation:\n\n${placesInfo}\n\nFormat: List with emojis, brief and attractive.`; + + try { + return await this.kimiService.chatWithTravelAssistant(prompt, [], language); + } catch (error) { + // Fallback simple + const formatted = places.map((place, index) => + `${index + 1}. **${place.name}** (${place.rating}/5) - ${place.description?.substring(0, 100)}...` + ).join('\n\n'); + + return language === 'es' + ? `Basado en tu ubicación y preferencias, aquí están mis recomendaciones:\n\n${formatted}` + : `Based on your location and preferences, here are my top recommendations:\n\n${formatted}`; + } + } + + private async findNearbyARContent(latitude: number, longitude: number, radius: number = 100): Promise { + return this.arContentRepository.find({ + where: { isActive: true }, + order: { viewsCount: 'DESC' }, + take: 10, + }); + } + + // ✅ ACTUALIZADO: Usar Kimi para generar audio guide + private async generateAudioGuide(queryDto: AIQueryDto): Promise<{ transcript: string; audioUrl: string }> { + const language = queryDto.language || 'es'; + + const prompt = language === 'es' + ? `Genera un guión para un audio tour de 2 minutos sobre ${queryDto.placeId ? 'este lugar histórico' : 'la Zona Colonial de Santo Domingo'}. + Hazlo informativo pero entretenido, como si fueras un guía turístico experto.` + : `Generate a 2-minute audio tour script about ${queryDto.placeId ? 'this historic place' : 'the Colonial Zone of Santo Domingo'}. + Make it informative but entertaining, as if you were an expert tour guide.`; + + try { + const transcript = await this.kimiService.chatWithTravelAssistant(prompt, [], language); + + // TODO: Integrar con servicio de Text-to-Speech (AWS Polly, Google TTS, etc.) + const audioUrl = "https://karibeo-audio-guides.s3.amazonaws.com/placeholder-audio.mp3"; + + return { transcript, audioUrl }; + } catch (error) { + return { + transcript: language === 'es' + ? "Bienvenido a este histórico lugar. Permíteme contarte sobre su fascinante historia..." + : "Welcome to this historic location. Let me tell you about its fascinating history...", + audioUrl: "https://karibeo-audio-guides.s3.amazonaws.com/placeholder-audio.mp3" + }; + } + } + + private async getSmartDirections(queryDto: AIQueryDto): Promise<{ instructions: string; waypoints: PlaceOfInterest[] }> { + const language = queryDto.language || 'es'; + + const instructions = language === 'es' + ? "Dirígete hacia el norte por 200 metros, luego gira a la derecha en la plaza histórica. Pasarás por varios puntos de interés en el camino." + : "Head north for 200 meters, then turn right at the historic plaza. You'll pass several interesting landmarks along the way."; + + const waypoints = await this.placeRepository.find({ + where: { active: true }, + take: 3, + }); + + return { instructions, waypoints }; + } + + private async getPersonalizedRecommendations(userId: string, queryDto: AIQueryDto): Promise { + return this.placeRepository.find({ + where: { active: true }, + order: { rating: 'DESC' }, + take: 5, + }); + } + + private async saveInteraction(interactionData: Partial): Promise { + const interaction = this.interactionRepository.create(interactionData); + await this.interactionRepository.save(interaction); + } + + // AR CONTENT MANAGEMENT + async getNearbyARContent(queryDto: ARContentQueryDto): Promise { + const query = this.arContentRepository.createQueryBuilder('ar') + .leftJoinAndSelect('ar.place', 'place') + .where('ar.isActive = :active', { active: true }); + + if (queryDto.contentType) { + query.andWhere('ar.contentType = :contentType', { contentType: queryDto.contentType }); + } + + if (queryDto.language) { + query.andWhere(':language = ANY(ar.languages)', { language: queryDto.language }); + } + + return query + .orderBy('ar.viewsCount', 'DESC') + .limit(10) + .getMany(); + } + + async incrementARViewCount(arContentId: string): Promise { + await this.arContentRepository.increment({ id: arContentId }, 'viewsCount', 1); + } + + // ✅ NUEVO: Generar itinerario completo con Kimi + async generateItinerary(params: { + userId: string; + destination: string; + days: number; + interests: string[]; + budget: string; + language: string; + }): Promise { + const { userId, destination, days, interests, budget, language } = params; + + try { + const itinerary = await this.kimiService.generateItinerary({ + destination, + days, + interests, + budget, + language, + }); + + // Guardar la interacción + await this.saveInteraction({ + userId, + userQuery: `Generate itinerary: ${destination}, ${days} days`, + aiResponse: itinerary, + interactionType: 'itinerary-generation' as any, + language, + metadata: { destination, days, interests, budget }, + }); + + return itinerary; + } catch (error) { + this.logger.error(`Itinerary generation error: ${error.message}`); + throw new BadRequestException('Failed to generate itinerary'); + } + } + + // ANALYTICS + async getAIUsageStats(): Promise<{ + totalInteractions: number; + byType: Array<{ type: string; count: number }>; + byLanguage: Array<{ language: string; count: number }>; + averageRating: number; + popularQueries: Array<{ query: string; count: number }>; + }> { + const [totalInteractions, byType, byLanguage, avgRating] = await Promise.all([ + this.interactionRepository.count(), + this.interactionRepository + .createQueryBuilder('interaction') + .select('interaction.interactionType', 'type') + .addSelect('COUNT(*)', 'count') + .groupBy('interaction.interactionType') + .getRawMany(), + this.interactionRepository + .createQueryBuilder('interaction') + .select('interaction.language', 'language') + .addSelect('COUNT(*)', 'count') + .groupBy('interaction.language') + .getRawMany(), + this.interactionRepository + .createQueryBuilder('interaction') + .select('AVG(interaction.rating)', 'average') + .where('interaction.rating IS NOT NULL') + .getRawOne(), + ]); + + const popularQueries = await this.interactionRepository + .createQueryBuilder('interaction') + .select('interaction.userQuery', 'query') + .addSelect('COUNT(*)', 'count') + .groupBy('interaction.userQuery') + .orderBy('count', 'DESC') + .limit(10) + .getRawMany(); + + return { + totalInteractions, + byType: byType.map(item => ({ type: item.type, count: parseInt(item.count) })), + byLanguage: byLanguage.map(item => ({ language: item.language, count: parseInt(item.count) })), + averageRating: parseFloat(avgRating?.average || '0'), + popularQueries: popularQueries.map(item => ({ query: item.query, count: parseInt(item.count) })), + }; + } + + // ✅ NUEVO: Limpiar cache de conversación + clearConversationCache(sessionId: string): void { + this.conversationCache.delete(sessionId); + } + + /** + * Determinar ciudad aproximada basada en coordenadas + */ + private getCityFromCoordinates(lat: number, lng: number): string { + // Santo Domingo area: lat 18.4-18.6, lng -70.0 to -69.8 + if (lat >= 18.4 && lat <= 18.6 && lng >= -70.0 && lng <= -69.8) { + return 'Santo Domingo'; + } + // Punta Cana area: lat 18.5-18.7, lng -68.5 to -68.3 + if (lat >= 18.5 && lat <= 18.7 && lng >= -68.5 && lng <= -68.3) { + return 'Punta Cana'; + } + // Santiago area: lat 19.4-19.5, lng -70.7 to -70.6 + if (lat >= 19.4 && lat <= 19.5 && lng >= -70.7 && lng <= -70.6) { + return 'Santiago'; + } + // Puerto Plata area: lat 19.7-19.9, lng -70.7 to -70.6 + if (lat >= 19.7 && lat <= 19.9 && lng >= -70.7 && lng <= -70.6) { + return 'Puerto Plata'; + } + // Samaná area: lat 19.2-19.3, lng -69.4 to -69.2 + if (lat >= 19.2 && lat <= 19.3 && lng >= -69.4 && lng <= -69.2) { + return 'Samaná'; + } + // La Romana area + if (lat >= 18.4 && lat <= 18.5 && lng >= -69.0 && lng <= -68.9) { + return 'La Romana'; + } + return 'República Dominicana'; + } + +} diff --git a/src/modules/ai-guide/dto/ai-query.dto.ts b/src/modules/ai-guide/dto/ai-query.dto.ts index f5c1d20..33262ed 100644 --- a/src/modules/ai-guide/dto/ai-query.dto.ts +++ b/src/modules/ai-guide/dto/ai-query.dto.ts @@ -23,6 +23,10 @@ export class AIQueryDto { @IsOptional() @IsString() placeId?: string; + @ApiPropertyOptional({ description: 'Place name for audio guide' }) + @IsOptional() + @IsString() + placeName?: string; @ApiPropertyOptional({ description: 'User latitude' }) @IsOptional() diff --git a/src/modules/ai-guide/tts.controller.ts b/src/modules/ai-guide/tts.controller.ts new file mode 100644 index 0000000..219dcd9 --- /dev/null +++ b/src/modules/ai-guide/tts.controller.ts @@ -0,0 +1,252 @@ +import { Controller, Get, Post, Param, Body, Res, Logger, HttpException, HttpStatus } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiParam, ApiResponse, ApiBody } from '@nestjs/swagger'; +import { Response } from 'express'; +import { TTSService } from './tts.service'; +import { KimiService } from '../kimi/kimi.service'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as crypto from 'crypto'; + +interface AudioGuideCache { + audioUrl: string; + transcript: string; + language: string; + placeName: string; + createdAt: number; +} + +@ApiTags('TTS') +@Controller('tts') +export class TTSController { + private readonly logger = new Logger(TTSController.name); + private readonly cachePath: string; + private readonly cacheFile: string; + + constructor( + private readonly ttsService: TTSService, + private readonly kimiService: KimiService, + ) { + this.cachePath = '/home/karibeo-api/karibeo-api/tts/cache'; + this.cacheFile = path.join(this.cachePath, 'audio_guides.json'); + } + + /** + * Leer cache desde archivo JSON (siempre fresco para PM2 cluster mode) + */ + private readCache(): Map { + try { + if (fs.existsSync(this.cacheFile)) { + const data = fs.readFileSync(this.cacheFile, 'utf-8'); + const parsed = JSON.parse(data); + return new Map(Object.entries(parsed)); + } + } catch (error) { + this.logger.warn(`Failed to read cache: ${error.message}`); + } + return new Map(); + } + + /** + * Guardar cache a archivo JSON + */ + private saveCache(cache: Map): void { + try { + const obj: Record = {}; + cache.forEach((value, key) => { + obj[key] = value; + }); + fs.writeFileSync(this.cacheFile, JSON.stringify(obj, null, 2), 'utf-8'); + } catch (error) { + this.logger.error(`Failed to save cache: ${error.message}`); + } + } + + /** + * Generar clave de cache: hash de placeName normalizado + language + */ + private getCacheKey(placeName: string, language: string): string { + const normalized = placeName.toLowerCase().trim().replace(/\s+/g, '_'); + return crypto.createHash('md5').update(`${normalized}:${language}`).digest('hex'); + } + + @Get('audio/:fileName') + @ApiOperation({ summary: 'Get generated audio file' }) + @ApiParam({ name: 'fileName', description: 'Audio file name (hash.wav)' }) + @ApiResponse({ status: 200, description: 'Audio file stream' }) + @ApiResponse({ status: 404, description: 'Audio file not found' }) + async getAudioFile( + @Param('fileName') fileName: string, + @Res() res: Response, + ) { + // Validar nombre de archivo (solo permitir hash.wav) + if (!/^[a-f0-9]{32}\.wav$/.test(fileName)) { + throw new HttpException('Invalid file name', HttpStatus.BAD_REQUEST); + } + + const filePath = this.ttsService.getAudioFilePath(fileName); + + if (!filePath) { + this.logger.warn(`Audio file not found: ${fileName}`); + throw new HttpException('Audio file not found', HttpStatus.NOT_FOUND); + } + + // Obtener tamaño del archivo + const stat = fs.statSync(filePath); + + // Configurar headers para audio + res.set({ + 'Content-Type': 'audio/wav', + 'Content-Length': stat.size, + 'Accept-Ranges': 'bytes', + 'Cache-Control': 'public, max-age=86400', // Cache 1 día + }); + + // Stream del archivo + const readStream = fs.createReadStream(filePath); + readStream.pipe(res); + } + + @Get('status') + @ApiOperation({ summary: 'Check TTS service status' }) + @ApiResponse({ status: 200, description: 'TTS service status' }) + getStatus() { + const cache = this.readCache(); + return { + available: this.ttsService.isAvailable(), + message: this.ttsService.isAvailable() + ? 'Piper TTS is ready' + : 'Piper TTS is not available', + cachedGuides: cache.size, + }; + } + + @Post('generate') + @ApiOperation({ summary: 'Generate audio guide for a place (public - no auth required)' }) + @ApiBody({ + schema: { + type: 'object', + required: ['placeName'], + properties: { + placeName: { type: 'string', example: 'Catedral Primada de América' }, + language: { type: 'string', example: 'es', enum: ['es', 'en', 'fr', 'it', 'de'] }, + }, + }, + }) + @ApiResponse({ + status: 201, + description: 'Audio guide generated', + schema: { + type: 'object', + properties: { + audioUrl: { type: 'string' }, + transcript: { type: 'string' }, + language: { type: 'string' }, + placeName: { type: 'string' }, + cached: { type: 'boolean' }, + }, + }, + }) + async generateAudioGuide(@Body() body: { placeName: string; language?: string }) { + const { placeName, language = 'es' } = body; + + if (!placeName || placeName.trim().length < 3) { + throw new HttpException('Place name is required (min 3 characters)', HttpStatus.BAD_REQUEST); + } + + if (!this.ttsService.isAvailable()) { + throw new HttpException('TTS service not available', HttpStatus.SERVICE_UNAVAILABLE); + } + + // Leer cache desde archivo (fresco para PM2 cluster mode) + const cache = this.readCache(); + const cacheKey = this.getCacheKey(placeName, language); + const cached = cache.get(cacheKey); + + if (cached) { + // Verificar que el archivo de audio todavía existe + const audioFileName = cached.audioUrl.split('/').pop(); + if (audioFileName && this.ttsService.getAudioFilePath(audioFileName)) { + this.logger.log(`Cache HIT for: ${placeName} (${language})`); + return { + ...cached, + cached: true, + }; + } else { + // El archivo fue eliminado, remover del cache + cache.delete(cacheKey); + this.saveCache(cache); + this.logger.log(`Cache invalidated (audio missing): ${placeName} (${language})`); + } + } + + this.logger.log(`Cache MISS - Generating audio guide for: ${placeName} (${language})`); + + try { + // Prompts en español LATINOAMERICANO (con emojis permitidos, el TTS los filtra) + const prompts: Record = { + es: `Genera un guion corto (maximo 200 palabras) para un audio tour sobre ${placeName} en Republica Dominicana. +Hazlo informativo pero entretenido, como si fueras un guia turistico dominicano experto. +Incluye historia breve y un dato curioso. Puedes usar emojis para hacerlo mas visual. +Habla en segunda persona directamente al turista usando "tu" (NO uses "vosotros" ni expresiones de Espana). +Usa espanol latinoamericano caribeño, natural y amigable. +IMPORTANTE: NO uses caracteres chinos, japoneses ni de otros idiomas asiaticos.`, + en: `Generate a short script (max 200 words) for an audio tour about ${placeName} in the Dominican Republic. +Make it informative but entertaining, as if you were an expert Dominican tour guide. +Include brief history and a fun fact. You can use emojis to make it more visual. +Speak directly to the tourist in second person. +IMPORTANT: Do NOT use Chinese, Japanese or other Asian language characters.`, + fr: `Genere un script court (max 200 mots) pour une visite audio de ${placeName} en Republique Dominicaine. +Rends-le informatif mais divertissant, comme si tu etais un guide touristique expert. +Inclus une breve histoire et une anecdote. Tu peux utiliser des emojis. +IMPORTANT: N'utilise PAS de caracteres chinois, japonais ou d'autres langues asiatiques.`, + it: `Genera uno script breve (max 200 parole) per un tour audio su ${placeName} nella Repubblica Dominicana. +Rendilo informativo ma divertente, come se fossi una guida turistica esperta. +Includi una breve storia e una curiosita. Puoi usare emoji. +IMPORTANTE: NON usare caratteri cinesi, giapponesi o di altre lingue asiatiche.`, + de: `Erstelle ein kurzes Skript (max 200 Worter) fur eine Audio-Tour uber ${placeName} in der Dominikanischen Republik. +Mache es informativ aber unterhaltsam, als warst du ein erfahrener Reisefuhrer. +Fuge kurze Geschichte und eine interessante Tatsache hinzu. Du kannst Emojis verwenden. +WICHTIG: Verwende KEINE chinesischen, japanischen oder andere asiatische Schriftzeichen.`, + }; + + const prompt = prompts[language] || prompts['en']; + const transcript = await this.kimiService.chatWithTravelAssistant(prompt, [], language); + + // 2. Convert to audio with Piper TTS (el service limpia emojis/CJK) + const audioUrl = await this.ttsService.generateSpeech(transcript, language); + + if (!audioUrl) { + throw new HttpException('Failed to generate audio', HttpStatus.INTERNAL_SERVER_ERROR); + } + + // 3. Guardar en cache (re-leer para evitar race conditions) + const freshCache = this.readCache(); + const cacheEntry: AudioGuideCache = { + audioUrl, + transcript, + language, + placeName, + createdAt: Date.now(), + }; + freshCache.set(cacheKey, cacheEntry); + this.saveCache(freshCache); + + this.logger.log(`Audio guide cached: ${placeName} (${language})`); + + return { + audioUrl, + transcript, + language, + placeName, + cached: false, + }; + + } catch (error) { + this.logger.error(`Audio guide generation failed: ${error.message}`); + throw new HttpException( + `Failed to generate audio guide: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } +} diff --git a/src/modules/ai-guide/tts.service.ts b/src/modules/ai-guide/tts.service.ts new file mode 100644 index 0000000..49736dd --- /dev/null +++ b/src/modules/ai-guide/tts.service.ts @@ -0,0 +1,215 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as crypto from 'crypto'; + +const execAsync = promisify(exec); + +@Injectable() +export class TTSService { + private readonly logger = new Logger(TTSService.name); + private readonly piperPath: string; + private readonly voicesPath: string; + private readonly cachePath: string; + private readonly baseUrl: string; + + // Mapa de idiomas a voces Piper + private readonly voiceMap: Record = { + es: 'es_ES-davefx-medium', + en: 'en_US-amy-medium', + fr: 'fr_FR-siwis-medium', + it: 'it_IT-riccardo-x_low', + de: 'de_DE-thorsten-medium', + }; + + constructor() { + const basePath = '/home/karibeo-api/karibeo-api/tts'; + this.piperPath = path.join(basePath, 'piper', 'piper'); + this.voicesPath = path.join(basePath, 'voices'); + this.cachePath = path.join(basePath, 'cache'); + this.baseUrl = process.env.API_BASE_URL || 'https://api.karibeo.ai:8443'; + + // Crear directorio de cache si no existe + if (!fs.existsSync(this.cachePath)) { + fs.mkdirSync(this.cachePath, { recursive: true }); + } + + // Verificar que Piper existe + if (fs.existsSync(this.piperPath)) { + this.logger.log('Piper TTS initialized'); + } else { + this.logger.error('Piper binary not found at: ' + this.piperPath); + } + } + + /** + * Limpiar texto para síntesis de voz: + * - Eliminar emojis + * - Eliminar caracteres chinos/japoneses/coreanos + * - Eliminar markdown + * - Eliminar símbolos especiales + */ + private cleanTextForSpeech(text: string): string { + return text + // Eliminar caracteres CJK (Chino, Japonés, Coreano) + .replace(/[\u4E00-\u9FFF]/g, '') // CJK Unified Ideographs (chino) + .replace(/[\u3400-\u4DBF]/g, '') // CJK Extension A + .replace(/[\u{20000}-\u{2A6DF}]/gu, '') // CJK Extension B + .replace(/[\u3040-\u309F]/g, '') // Hiragana (japonés) + .replace(/[\u30A0-\u30FF]/g, '') // Katakana (japonés) + .replace(/[\uAC00-\uD7AF]/g, '') // Hangul (coreano) + .replace(/[\u1100-\u11FF]/g, '') // Hangul Jamo + // Eliminar emojis Unicode (rangos principales) + .replace(/[\u{1F300}-\u{1F9FF}]/gu, '') // Símbolos misceláneos y pictogramas + .replace(/[\u{2600}-\u{26FF}]/gu, '') // Símbolos misceláneos + .replace(/[\u{2700}-\u{27BF}]/gu, '') // Dingbats + .replace(/[\u{1F600}-\u{1F64F}]/gu, '') // Emoticonos + .replace(/[\u{1F680}-\u{1F6FF}]/gu, '') // Transporte y símbolos de mapa + .replace(/[\u{1F1E0}-\u{1F1FF}]/gu, '') // Banderas + .replace(/[\u{1FA00}-\u{1FAFF}]/gu, '') // Símbolos extendidos + .replace(/[\u{1F900}-\u{1F9FF}]/gu, '') // Emojis suplementarios + .replace(/[\u{231A}-\u{23FF}]/gu, '') // Símbolos técnicos + .replace(/[\u{25A0}-\u{25FF}]/gu, '') // Formas geométricas + // Eliminar otros símbolos que podrían leerse mal + .replace(/[★☆✓✗✔✘●○◆◇▪▫►◄→←↑↓⇒⇐⇑⇓♠♣♥♦]/g, '') + // Limpiar asteriscos de markdown bold/italic + .replace(/\*\*([^*]+)\*\*/g, '$1') // **bold** -> bold + .replace(/\*([^*]+)\*/g, '$1') // *italic* -> italic + .replace(/__([^_]+)__/g, '$1') // __bold__ -> bold + .replace(/_([^_]+)_/g, '$1') // _italic_ -> italic + // Eliminar múltiples espacios creados por eliminación + .replace(/\s+/g, ' ') + .trim(); + } + + /** + * Genera un hash único para el texto + idioma + */ + private getAudioHash(text: string, language: string): string { + const content = `${language}:${text}`; + return crypto.createHash('md5').update(content).digest('hex'); + } + + /** + * Obtiene la ruta del modelo de voz para el idioma + */ + private getVoiceModel(language: string): string { + const voiceName = this.voiceMap[language] || this.voiceMap['en']; + return path.join(this.voicesPath, `${voiceName}.onnx`); + } + + /** + * Genera speech usando Piper TTS local + * Retorna URL al archivo de audio + */ + async generateSpeech(text: string, language: string = 'es'): Promise { + if (!fs.existsSync(this.piperPath)) { + this.logger.error('Piper not available'); + return null; + } + + try { + // Verificar cache + const hash = this.getAudioHash(text, language); + const fileName = `${hash}.wav`; + const filePath = path.join(this.cachePath, fileName); + + // Si ya existe en cache, retornar URL + if (fs.existsSync(filePath)) { + this.logger.log(`Cache hit: ${fileName}`); + return `${this.baseUrl}/api/v1/tts/audio/${fileName}`; + } + + // Obtener modelo de voz + const voiceModel = this.getVoiceModel(language); + if (!fs.existsSync(voiceModel)) { + this.logger.error(`Voice model not found: ${voiceModel}`); + return null; + } + + // Limpiar texto: quitar emojis, CJK, y formateo markdown para el audio + const cleanText = this.cleanTextForSpeech(text) + .replace(/[\n\r]/g, ' ') + .replace(/"/g, "'") + .substring(0, 3000); // Límite de caracteres + + this.logger.log(`Clean text for TTS: ${cleanText.substring(0, 100)}...`); + + const tempTextFile = path.join(this.cachePath, `${hash}.txt`); + fs.writeFileSync(tempTextFile, cleanText, 'utf-8'); + + this.logger.log(`Generating TTS (${language}): ${cleanText.substring(0, 50)}...`); + + // Ejecutar Piper usando archivo de texto (timeout 120s para textos largos) + const command = `cat "${tempTextFile}" | "${this.piperPath}" --model "${voiceModel}" --output_file "${filePath}" 2>/dev/null`; + + await execAsync(command, { timeout: 120000 }); + + // Limpiar archivo temporal + if (fs.existsSync(tempTextFile)) { + fs.unlinkSync(tempTextFile); + } + + // Verificar que se generó el archivo + if (fs.existsSync(filePath)) { + const stats = fs.statSync(filePath); + this.logger.log(`Audio generated: ${fileName} (${stats.size} bytes)`); + return `${this.baseUrl}/api/v1/tts/audio/${fileName}`; + } + + this.logger.error('Audio file was not created'); + return null; + + } catch (error) { + this.logger.error(`TTS generation failed: ${error.message}`); + return null; + } + } + + /** + * Obtiene la ruta del archivo de audio para servir + */ + getAudioFilePath(fileName: string): string | null { + const filePath = path.join(this.cachePath, fileName); + if (fs.existsSync(filePath)) { + return filePath; + } + return null; + } + + /** + * Limpia archivos de cache antiguos (más de 7 días) + */ + async cleanOldCache(): Promise { + let cleaned = 0; + const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 días en ms + const now = Date.now(); + + try { + const files = fs.readdirSync(this.cachePath); + for (const file of files) { + if (file.endsWith('.txt')) continue; // Skip temp files + const filePath = path.join(this.cachePath, file); + const stats = fs.statSync(filePath); + if (now - stats.mtimeMs > maxAge) { + fs.unlinkSync(filePath); + cleaned++; + } + } + this.logger.log(`Cleaned ${cleaned} old audio files`); + } catch (error) { + this.logger.error(`Cache cleanup failed: ${error.message}`); + } + + return cleaned; + } + + /** + * Verifica si TTS está disponible + */ + isAvailable(): boolean { + return fs.existsSync(this.piperPath); + } +} diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index c9c8382..6596b1b 100755 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,110 +1,202 @@ -import { Controller, Post, Body, HttpCode, HttpStatus, UseGuards, Get, Request } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiBody } from '@nestjs/swagger'; -import { AuthService } from './auth.service'; -import { RegisterDto } from './dto/register.dto'; -import { LoginDto } from './dto/login.dto'; -import { AuthResponseDto } from './dto/auth-response.dto'; -import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; -import { User } from '../../entities/user.entity'; - -@ApiTags('Authentication') -@Controller('auth') -export class AuthController { - constructor(private readonly authService: AuthService) {} - - @Post('register') - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ - summary: 'Register a new user', - description: 'Creates a new user account with tourist role by default' - }) - @ApiBody({ type: RegisterDto }) - @ApiResponse({ - status: 201, - description: 'User successfully registered', - type: AuthResponseDto - }) - @ApiResponse({ - status: 409, - description: 'User with this email already exists' - }) - @ApiResponse({ - status: 400, - description: 'Invalid input data' - }) - async register(@Body() registerDto: RegisterDto): Promise { - return this.authService.register(registerDto); - } - - @Post('login') - @HttpCode(HttpStatus.OK) - @ApiOperation({ - summary: 'User login', - description: 'Authenticates user and returns JWT tokens' - }) - @ApiBody({ type: LoginDto }) - @ApiResponse({ - status: 200, - description: 'User successfully authenticated', - type: AuthResponseDto - }) - @ApiResponse({ - status: 401, - description: 'Invalid credentials or account locked' - }) - async login(@Body() loginDto: LoginDto): Promise { - return this.authService.login(loginDto); - } - - @Post('refresh') - @HttpCode(HttpStatus.OK) - @ApiOperation({ - summary: 'Refresh access token', - description: 'Refresh JWT access token using refresh token' - }) - @ApiBody({ - schema: { - type: 'object', - properties: { - refreshToken: { - type: 'string', - description: 'Valid refresh token', - example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' - } - }, - required: ['refreshToken'] - } - }) - @ApiResponse({ - status: 200, - description: 'Tokens refreshed successfully', - type: AuthResponseDto - }) - @ApiResponse({ - status: 401, - description: 'Invalid or expired refresh token' - }) - async refresh(@Body() body: { refreshToken: string }): Promise { - return this.authService.refresh(body.refreshToken); - } - - @Get('profile') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth('JWT-auth') - @ApiOperation({ - summary: 'Get current user profile', - description: 'Returns the profile of the authenticated user' - }) - @ApiResponse({ - status: 200, - description: 'User profile retrieved successfully', - type: User - }) - @ApiResponse({ - status: 401, - description: 'Unauthorized - Invalid or missing token' - }) - async getProfile(@Request() req): Promise { - return req.user; - } -} \ No newline at end of file +import { Controller, Post, Body, HttpCode, HttpStatus, UseGuards, Get, Request, Res, Query } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiBody } from "@nestjs/swagger"; +import { Response } from "express"; +import { AuthGuard } from "@nestjs/passport"; +import { ConfigService } from "@nestjs/config"; +import { AuthService } from "./auth.service"; +import { RegisterDto } from "./dto/register.dto"; +import { LoginDto } from "./dto/login.dto"; +import { AuthResponseDto } from "./dto/auth-response.dto"; +import { JwtAuthGuard } from "../../common/guards/jwt-auth.guard"; +import { User } from "../../entities/user.entity"; +import { AppleStrategy } from "./strategies/apple.strategy"; + +@ApiTags("Authentication") +@Controller("auth") +export class AuthController { + constructor( + private readonly authService: AuthService, + private readonly configService: ConfigService, + private readonly appleStrategy: AppleStrategy, + ) {} + + private getFrontendUrl(): string { + return this.configService.get("FRONTEND_URL") || "https://karibeo.ai"; + } + + @Post("register") + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ + summary: "Register a new user", + description: "Creates a new user account with tourist role by default" + }) + @ApiBody({ type: RegisterDto }) + @ApiResponse({ + status: 201, + description: "User successfully registered", + type: AuthResponseDto + }) + @ApiResponse({ + status: 409, + description: "User with this email already exists" + }) + @ApiResponse({ + status: 400, + description: "Invalid input data" + }) + async register(@Body() registerDto: RegisterDto): Promise { + return this.authService.register(registerDto); + } + + @Post("login") + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: "User login", + description: "Authenticates user and returns JWT tokens" + }) + @ApiBody({ type: LoginDto }) + @ApiResponse({ + status: 200, + description: "User successfully authenticated", + type: AuthResponseDto + }) + @ApiResponse({ + status: 401, + description: "Invalid credentials or account locked" + }) + async login(@Body() loginDto: LoginDto): Promise { + return this.authService.login(loginDto); + } + + @Post("refresh") + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: "Refresh access token", + description: "Refresh JWT access token using refresh token" + }) + @ApiBody({ + schema: { + type: "object", + properties: { + refreshToken: { + type: "string", + description: "Valid refresh token", + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } + }, + required: ["refreshToken"] + } + }) + @ApiResponse({ + status: 200, + description: "Tokens refreshed successfully", + type: AuthResponseDto + }) + @ApiResponse({ + status: 401, + description: "Invalid or expired refresh token" + }) + async refresh(@Body() body: { refreshToken: string }): Promise { + return this.authService.refresh(body.refreshToken); + } + + @Get("profile") + @UseGuards(JwtAuthGuard) + @ApiBearerAuth("JWT-auth") + @ApiOperation({ + summary: "Get current user profile", + description: "Returns the profile of the authenticated user" + }) + @ApiResponse({ + status: 200, + description: "User profile retrieved successfully", + type: User + }) + @ApiResponse({ + status: 401, + description: "Unauthorized - Invalid or missing token" + }) + async getProfile(@Request() req): Promise { + return req.user; + } + + // ==================== GOOGLE OAUTH ==================== + + @Get("google") + @UseGuards(AuthGuard("google")) + @ApiOperation({ summary: "Initiate Google OAuth login" }) + async googleAuth() { + // Guard redirects to Google + } + + @Get("google/callback") + @UseGuards(AuthGuard("google")) + @ApiOperation({ summary: "Google OAuth callback" }) + async googleAuthCallback(@Request() req, @Res() res: Response) { + try { + const result = await this.authService.handleOAuthLogin({ + email: req.user.email, + firstName: req.user.firstName, + lastName: req.user.lastName, + profileImageUrl: req.user.profileImageUrl, + provider: "google", + providerId: req.user.providerId, + }); + + // Redirect to frontend with tokens + const frontendUrl = this.getFrontendUrl(); + const redirectUrl = `${frontendUrl}?accessToken=${result.accessToken}&refreshToken=${result.refreshToken}`; + return res.redirect(redirectUrl); + } catch (error) { + const frontendUrl = this.getFrontendUrl(); + return res.redirect(`${frontendUrl}?error=oauth_failed`); + } + } + + // ==================== APPLE OAUTH ==================== + + @Get("apple") + @ApiOperation({ summary: "Initiate Apple Sign In" }) + async appleAuth(@Res() res: Response) { + const authUrl = this.appleStrategy.getAuthorizationUrl(); + return res.redirect(authUrl); + } + + @Post("apple/callback") + @ApiOperation({ summary: "Apple Sign In callback" }) + async appleAuthCallback( + @Body() body: { id_token: string; code?: string; user?: string }, + @Res() res: Response, + ) { + try { + const appleUser = await this.appleStrategy.validateToken(body.id_token, body.code); + + // Parse user info if provided (only on first login) + let firstName = "Usuario"; + let lastName = "Karibeo"; + if (body.user) { + try { + const userData = JSON.parse(body.user); + firstName = userData.name?.firstName || firstName; + lastName = userData.name?.lastName || lastName; + } catch {} + } + + const result = await this.authService.handleOAuthLogin({ + email: appleUser.email, + firstName, + lastName, + provider: "apple", + providerId: appleUser.providerId, + }); + + // Redirect to frontend with tokens + const frontendUrl = this.getFrontendUrl(); + const redirectUrl = `${frontendUrl}?accessToken=${result.accessToken}&refreshToken=${result.refreshToken}`; + return res.redirect(redirectUrl); + } catch (error) { + const frontendUrl = this.getFrontendUrl(); + return res.redirect(`${frontendUrl}?error=oauth_failed`); + } + } +} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 054813c..cd28b39 100755 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -1,30 +1,33 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { JwtModule } from '@nestjs/jwt'; -import { PassportModule } from '@nestjs/passport'; -import { ConfigService } from '@nestjs/config'; -import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; -import { JwtStrategy } from './strategies/jwt.strategy'; -import { User } from '../../entities/user.entity'; -import { Role } from '../../entities/role.entity'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([User, Role]), - PassportModule.register({ defaultStrategy: 'jwt' }), - JwtModule.registerAsync({ - useFactory: (configService: ConfigService) => ({ - secret: configService.get('JWT_SECRET'), - signOptions: { - expiresIn: configService.get('JWT_EXPIRES_IN') || '24h', - }, - }), - inject: [ConfigService], - }), - ], - controllers: [AuthController], - providers: [AuthService, JwtStrategy], - exports: [AuthService, JwtStrategy, PassportModule], -}) -export class AuthModule {} +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { JwtModule } from "@nestjs/jwt"; +import { PassportModule } from "@nestjs/passport"; +import { ConfigModule, ConfigService } from "@nestjs/config"; +import { AuthController } from "./auth.controller"; +import { AuthService } from "./auth.service"; +import { JwtStrategy } from "./strategies/jwt.strategy"; +import { GoogleStrategy } from "./strategies/google.strategy"; +import { AppleStrategy } from "./strategies/apple.strategy"; +import { User } from "../../entities/user.entity"; +import { Role } from "../../entities/role.entity"; + +@Module({ + imports: [ + ConfigModule, + TypeOrmModule.forFeature([User, Role]), + PassportModule.register({ defaultStrategy: "jwt" }), + JwtModule.registerAsync({ + useFactory: (configService: ConfigService) => ({ + secret: configService.get("JWT_SECRET"), + signOptions: { + expiresIn: configService.get("JWT_EXPIRES_IN") || "24h", + }, + }), + inject: [ConfigService], + }), + ], + controllers: [AuthController], + providers: [AuthService, JwtStrategy, GoogleStrategy, AppleStrategy], + exports: [AuthService, JwtStrategy, PassportModule], +}) +export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index d5bc701..2ab7a9f 100755 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,173 +1,210 @@ -import { Injectable, UnauthorizedException, ConflictException, InternalServerErrorException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { JwtService } from '@nestjs/jwt'; -import * as bcrypt from 'bcrypt'; -import { User } from '../../entities/user.entity'; -import { Role } from '../../entities/role.entity'; -import { RegisterDto } from './dto/register.dto'; -import { LoginDto } from './dto/login.dto'; -import { AuthResponseDto } from './dto/auth-response.dto'; - -@Injectable() -export class AuthService { - constructor( - @InjectRepository(User) - private readonly userRepository: Repository, - @InjectRepository(Role) - private readonly roleRepository: Repository, - private readonly jwtService: JwtService, - ) {} - - async register(registerDto: RegisterDto): Promise { - const { email, password, ...userData } = registerDto; - - // Check if user already exists - const existingUser = await this.userRepository.findOne({ where: { email } }); - if (existingUser) { - throw new ConflictException('User with this email already exists'); - } - - // Hash password - const saltRounds = 12; - const passwordHash = await bcrypt.hash(password, saltRounds); - - // Get default tourist role - const defaultRole = await this.roleRepository.findOne({ where: { name: 'tourist' } }); - - // Create user - const user = this.userRepository.create({ - email, - passwordHash, - roleId: defaultRole?.id || 2, // Default to tourist role - ...userData, - }); - - const savedUser = await this.userRepository.save(user); - - // Generate tokens - const { accessToken, refreshToken } = await this.generateTokens(savedUser); - - // Load user with relations for response - const userWithRelations = await this.userRepository.findOne({ - where: { id: savedUser.id }, - relations: ['country', 'role', 'preferredLanguageEntity'], - }); - - if (!userWithRelations) { - throw new InternalServerErrorException('Failed to retrieve user after registration'); - } - - return { - accessToken, - refreshToken, - user: userWithRelations, - expiresIn: '24h', - }; - } - - async login(loginDto: LoginDto): Promise { - const { email, password } = loginDto; - - // Find user with relations - const user = await this.userRepository.findOne({ - where: { email }, - relations: ['country', 'role', 'preferredLanguageEntity'], - }); - - if (!user) { - throw new UnauthorizedException('Invalid credentials'); - } - - // Check if account is locked - if (user.lockedUntil && new Date() < user.lockedUntil) { - throw new UnauthorizedException('Account is temporarily locked'); - } - - // Verify password - const isPasswordValid = await bcrypt.compare(password, user.passwordHash); - if (!isPasswordValid) { - // Increment failed login attempts - await this.handleFailedLogin(user); - throw new UnauthorizedException('Invalid credentials'); - } - - // Reset failed login attempts on successful login - await this.userRepository.update(user.id, { - failedLoginAttempts: 0, - lockedUntil: undefined, - lastLogin: new Date(), - }); - - // Generate tokens - const { accessToken, refreshToken } = await this.generateTokens(user); - - return { - accessToken, - refreshToken, - user, - expiresIn: '24h', - }; - } - - async validateUser(userId: string): Promise { - return this.userRepository.findOne({ - where: { id: userId, isActive: true }, - relations: ['country', 'role', 'preferredLanguageEntity'], - }); - } - - async refresh(refreshToken: string): Promise { - try { - // Verificar el refresh token - const payload = this.jwtService.verify(refreshToken); - - // Buscar el usuario - const user = await this.userRepository.findOne({ - where: { id: payload.sub, isActive: true }, - relations: ['country', 'role', 'preferredLanguageEntity'], - }); - - if (!user) { - throw new UnauthorizedException('User not found or inactive'); - } - - // Generar nuevos tokens - const { accessToken, refreshToken: newRefreshToken } = await this.generateTokens(user); - - return { - accessToken, - refreshToken: newRefreshToken, - user, - expiresIn: '24h', - }; - } catch (error) { - throw new UnauthorizedException('Invalid or expired refresh token'); - } - } - - private async generateTokens(user: User): Promise<{ accessToken: string; refreshToken: string }> { - const payload = { - sub: user.id, - email: user.email, - role: user.role?.name || 'tourist', - }; - - const accessToken = this.jwtService.sign(payload); - const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' }); - - return { accessToken, refreshToken }; - } - - private async handleFailedLogin(user: User): Promise { - const failedAttempts = user.failedLoginAttempts + 1; - const updateData: any = { failedLoginAttempts: failedAttempts }; - - // Lock account after 5 failed attempts for 15 minutes - if (failedAttempts >= 5) { - updateData.lockedUntil = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes - } - - await this.userRepository.update(user.id, updateData); - } -} \ No newline at end of file +import { Injectable, UnauthorizedException, ConflictException, InternalServerErrorException } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { JwtService } from "@nestjs/jwt"; +import * as bcrypt from "bcrypt"; +import { User } from "../../entities/user.entity"; +import { Role } from "../../entities/role.entity"; +import { RegisterDto } from "./dto/register.dto"; +import { LoginDto } from "./dto/login.dto"; +import { AuthResponseDto } from "./dto/auth-response.dto"; + +@Injectable() +export class AuthService { + constructor( + @InjectRepository(User) + private readonly userRepository: Repository, + @InjectRepository(Role) + private readonly roleRepository: Repository, + private readonly jwtService: JwtService, + ) {} + + async register(registerDto: RegisterDto): Promise { + const { email, password, ...userData } = registerDto; + + const existingUser = await this.userRepository.findOne({ where: { email } }); + if (existingUser) { + throw new ConflictException("User with this email already exists"); + } + + const saltRounds = 12; + const passwordHash = await bcrypt.hash(password, saltRounds); + + const defaultRole = await this.roleRepository.findOne({ where: { name: "tourist" } }); + + const user = this.userRepository.create({ + email, + passwordHash, + roleId: defaultRole?.id || 2, + ...userData, + }); + + const savedUser = await this.userRepository.save(user); + const { accessToken, refreshToken } = await this.generateTokens(savedUser); + + const userWithRelations = await this.userRepository.findOne({ + where: { id: savedUser.id }, + relations: ["country", "role", "preferredLanguageEntity"], + }); + + if (!userWithRelations) { + throw new InternalServerErrorException("Failed to retrieve user after registration"); + } + + return { + accessToken, + refreshToken, + user: userWithRelations, + expiresIn: "24h", + }; + } + + async login(loginDto: LoginDto): Promise { + const { email, password } = loginDto; + + const user = await this.userRepository.findOne({ + where: { email }, + relations: ["country", "role", "preferredLanguageEntity"], + }); + + if (!user) { + throw new UnauthorizedException("Invalid credentials"); + } + + if (user.lockedUntil && new Date() < user.lockedUntil) { + throw new UnauthorizedException("Account is temporarily locked"); + } + + const isPasswordValid = await bcrypt.compare(password, user.passwordHash); + if (!isPasswordValid) { + await this.handleFailedLogin(user); + throw new UnauthorizedException("Invalid credentials"); + } + + await this.userRepository.update(user.id, { + failedLoginAttempts: 0, + lockedUntil: undefined, + lastLogin: new Date(), + }); + + const { accessToken, refreshToken } = await this.generateTokens(user); + + return { + accessToken, + refreshToken, + user, + expiresIn: "24h", + }; + } + + async validateUser(userId: string): Promise { + return this.userRepository.findOne({ + where: { id: userId, isActive: true }, + relations: ["country", "role", "preferredLanguageEntity"], + }); + } + + async refresh(refreshToken: string): Promise { + try { + const payload = this.jwtService.verify(refreshToken); + + const user = await this.userRepository.findOne({ + where: { id: payload.sub, isActive: true }, + relations: ["country", "role", "preferredLanguageEntity"], + }); + + if (!user) { + throw new UnauthorizedException("User not found or inactive"); + } + + const { accessToken, refreshToken: newRefreshToken } = await this.generateTokens(user); + + return { + accessToken, + refreshToken: newRefreshToken, + user, + expiresIn: "24h", + }; + } catch (error) { + throw new UnauthorizedException("Invalid or expired refresh token"); + } + } + + async handleOAuthLogin(oauthUser: { + email: string; + firstName?: string; + lastName?: string; + profileImageUrl?: string; + provider: string; + providerId: string; + }): Promise { + let user = await this.userRepository.findOne({ + where: { email: oauthUser.email }, + relations: ["country", "role", "preferredLanguageEntity"], + }); + + if (!user) { + const defaultRole = await this.roleRepository.findOne({ where: { name: "tourist" } }); + + user = this.userRepository.create({ + email: oauthUser.email, + firstName: oauthUser.firstName || "Usuario", + lastName: oauthUser.lastName || "Karibeo", + passwordHash: "", + profileImageUrl: oauthUser.profileImageUrl, + roleId: defaultRole?.id || 2, + isVerified: true, + }); + + user = await this.userRepository.save(user); + + user = await this.userRepository.findOne({ + where: { id: user.id }, + relations: ["country", "role", "preferredLanguageEntity"], + }) as User; + } else { + if (oauthUser.profileImageUrl && !user.profileImageUrl) { + await this.userRepository.update(user.id, { + profileImageUrl: oauthUser.profileImageUrl, + lastLogin: new Date(), + }); + user.profileImageUrl = oauthUser.profileImageUrl; + } else { + await this.userRepository.update(user.id, { lastLogin: new Date() }); + } + } + + const { accessToken, refreshToken } = await this.generateTokens(user); + + return { + accessToken, + refreshToken, + user, + expiresIn: "24h", + }; + } + + private async generateTokens(user: User): Promise<{ accessToken: string; refreshToken: string }> { + const payload = { + sub: user.id, + email: user.email, + role: user.role?.name || "tourist", + }; + + const accessToken = this.jwtService.sign(payload); + const refreshToken = this.jwtService.sign(payload, { expiresIn: "7d" }); + + return { accessToken, refreshToken }; + } + + private async handleFailedLogin(user: User): Promise { + const failedAttempts = user.failedLoginAttempts + 1; + const updateData: any = { failedLoginAttempts: failedAttempts }; + + if (failedAttempts >= 5) { + updateData.lockedUntil = new Date(Date.now() + 15 * 60 * 1000); + } + + await this.userRepository.update(user.id, updateData); + } +} diff --git a/src/modules/auth/strategies/apple.strategy.ts b/src/modules/auth/strategies/apple.strategy.ts new file mode 100644 index 0000000..17b31f5 --- /dev/null +++ b/src/modules/auth/strategies/apple.strategy.ts @@ -0,0 +1,52 @@ +import { Injectable } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import * as appleSignin from "apple-signin-auth"; + +export interface AppleUser { + email: string; + firstName?: string; + lastName?: string; + provider: string; + providerId: string; +} + +@Injectable() +export class AppleStrategy { + constructor(private configService: ConfigService) {} + + getAuthorizationUrl(): string { + const clientId = this.configService.get("APPLE_CLIENT_ID") || ""; + const redirectUri = this.configService.get("APPLE_CALLBACK_URL") || + "https://api.karibeo.ai:8443/api/v1/auth/apple/callback"; + + const scope = "name email"; + const responseType = "code id_token"; + const responseMode = "form_post"; + + const params = new URLSearchParams({ + client_id: clientId, + redirect_uri: redirectUri, + response_type: responseType, + scope, + response_mode: responseMode, + }); + + return `https://appleid.apple.com/auth/authorize?${params.toString()}`; + } + + async validateToken(idToken: string, authorizationCode?: string): Promise { + const clientId = this.configService.get("APPLE_CLIENT_ID"); + + // Verify the Apple token + const payload = await appleSignin.verifyIdToken(idToken, { + audience: clientId, + ignoreExpiration: false, + }); + + return { + email: payload.email || "", + provider: "apple", + providerId: payload.sub, + }; + } +} diff --git a/src/modules/auth/strategies/google.strategy.ts b/src/modules/auth/strategies/google.strategy.ts new file mode 100644 index 0000000..b7d5de3 --- /dev/null +++ b/src/modules/auth/strategies/google.strategy.ts @@ -0,0 +1,35 @@ +import { Injectable } from "@nestjs/common"; +import { PassportStrategy } from "@nestjs/passport"; +import { Strategy, VerifyCallback, Profile } from "passport-google-oauth20"; +import { ConfigService } from "@nestjs/config"; + +@Injectable() +export class GoogleStrategy extends PassportStrategy(Strategy, "google") { + constructor(configService: ConfigService) { + super({ + clientID: configService.get("GOOGLE_CLIENT_ID") || "", + clientSecret: configService.get("GOOGLE_CLIENT_SECRET") || "", + callbackURL: configService.get("GOOGLE_CALLBACK_URL") || "https://api.karibeo.ai:8443/api/v1/auth/google/callback", + scope: ["email", "profile"], + }); + } + + async validate( + accessToken: string, + refreshToken: string, + profile: Profile, + done: VerifyCallback, + ): Promise { + const { name, emails, photos } = profile; + const user = { + email: emails?.[0]?.value, + firstName: name?.givenName || "", + lastName: name?.familyName || "", + profileImageUrl: photos?.[0]?.value, + provider: "google", + providerId: profile.id, + accessToken, + }; + done(null, user); + } +} diff --git a/src/modules/booking/booking.controller.ts b/src/modules/booking/booking.controller.ts new file mode 100644 index 0000000..f1f8455 --- /dev/null +++ b/src/modules/booking/booking.controller.ts @@ -0,0 +1,112 @@ +import { Controller, Get, Post, Query, Body, Param } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { BookingService } from './booking.service'; +import { SearchAccommodationsDto } from './dto/search-accommodations.dto'; +import { SearchFlightsDto } from './dto/search-flights.dto'; +import { SearchCarsDto } from './dto/search-cars.dto'; + +@ApiTags('Travel - Booking.com') +@Controller('booking') +export class BookingController { + constructor(private readonly bookingService: BookingService) {} + + // ==================== ACCOMMODATIONS ==================== + + @Get('accommodations/search') + @ApiOperation({ summary: 'Search for hotels and accommodations' }) + @ApiResponse({ status: 200, description: 'Accommodation search results' }) + async searchAccommodations(@Query() dto: SearchAccommodationsDto) { + return this.bookingService.searchAccommodations(dto); + } + + @Post('accommodations/search') + @ApiOperation({ summary: 'Search for hotels and accommodations (POST)' }) + async searchAccommodationsPost(@Body() dto: SearchAccommodationsDto) { + return this.bookingService.searchAccommodations(dto); + } + + @Get('accommodations/:id') + @ApiOperation({ summary: 'Get accommodation details' }) + async getAccommodationDetails(@Param('id') id: string) { + return this.bookingService.getAccommodationDetails(id); + } + + @Get('accommodations/:id/availability') + @ApiOperation({ summary: 'Check accommodation availability' }) + async checkAvailability( + @Param('id') id: string, + @Query('checkin') checkin: string, + @Query('checkout') checkout: string, + @Query('guests') guests: number, + ) { + return this.bookingService.checkAvailability(id, checkin, checkout, guests || 2); + } + + // ==================== FLIGHTS ==================== + + @Get('flights/search') + @ApiOperation({ summary: 'Search for flights' }) + @ApiResponse({ status: 200, description: 'Flight search results' }) + async searchFlights(@Query() dto: SearchFlightsDto) { + return this.bookingService.searchFlights(dto); + } + + @Post('flights/search') + @ApiOperation({ summary: 'Search for flights (POST)' }) + async searchFlightsPost(@Body() dto: SearchFlightsDto) { + return this.bookingService.searchFlights(dto); + } + + @Get('flights/:id') + @ApiOperation({ summary: 'Get flight details' }) + async getFlightDetails(@Param('id') id: string) { + return this.bookingService.getFlightDetails(id); + } + + // ==================== CAR RENTALS ==================== + + @Get('cars/search') + @ApiOperation({ summary: 'Search for car rentals' }) + @ApiResponse({ status: 200, description: 'Car rental search results' }) + async searchCars(@Query() dto: SearchCarsDto) { + return this.bookingService.searchCars(dto); + } + + @Post('cars/search') + @ApiOperation({ summary: 'Search for car rentals (POST)' }) + async searchCarsPost(@Body() dto: SearchCarsDto) { + return this.bookingService.searchCars(dto); + } + + @Get('cars/:id') + @ApiOperation({ summary: 'Get car rental details' }) + async getCarDetails(@Param('id') id: string) { + return this.bookingService.getCarDetails(id); + } + + @Get('cars/depots/:location') + @ApiOperation({ summary: 'Get car rental depots for a location' }) + async getCarDepots(@Param('location') location: string) { + return this.bookingService.getCarDepots(location); + } + + // ==================== LOCATIONS ==================== + + @Get('locations/search') + @ApiOperation({ summary: 'Search for cities, airports, hotels' }) + async searchLocations( + @Query('query') query: string, + @Query('type') type?: 'city' | 'airport' | 'hotel', + ) { + return this.bookingService.searchLocations(query, type); + } + + // ==================== STATUS ==================== + + @Get('status') + @ApiOperation({ summary: 'Get Booking.com API status' }) + @ApiResponse({ status: 200, description: 'API configuration status' }) + getStatus() { + return this.bookingService.getStatus(); + } +} diff --git a/src/modules/booking/booking.module.ts b/src/modules/booking/booking.module.ts new file mode 100644 index 0000000..774c254 --- /dev/null +++ b/src/modules/booking/booking.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { BookingService } from './booking.service'; +import { BookingController } from './booking.controller'; +import bookingConfig from '../../config/integrations/booking.config'; + +@Module({ + imports: [ + ConfigModule.forFeature(bookingConfig), + ], + controllers: [BookingController], + providers: [BookingService], + exports: [BookingService], +}) +export class BookingModule {} diff --git a/src/modules/booking/booking.service.ts b/src/modules/booking/booking.service.ts new file mode 100644 index 0000000..969571b --- /dev/null +++ b/src/modules/booking/booking.service.ts @@ -0,0 +1,236 @@ +import { Injectable, Logger, HttpException, HttpStatus, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { SearchAccommodationsDto } from './dto/search-accommodations.dto'; +import { SearchFlightsDto, TripType } from './dto/search-flights.dto'; +import { SearchCarsDto } from './dto/search-cars.dto'; + +interface BookingConfig { + affiliateId: string; + apiKey: string; + baseUrl: string; + environment: string; + defaultCurrency: string; + defaultLanguage: string; +} + +@Injectable() +export class BookingService implements OnModuleInit { + private readonly logger = new Logger(BookingService.name); + private config: BookingConfig; + + constructor(private readonly configService: ConfigService) {} + + onModuleInit() { + this.config = { + affiliateId: this.configService.get('booking.affiliateId') || '', + apiKey: this.configService.get('booking.apiKey') || '', + baseUrl: this.configService.get('booking.baseUrl') || 'https://demandapi.booking.com/3.1', + environment: this.configService.get('booking.environment') || 'sandbox', + defaultCurrency: this.configService.get('booking.defaultCurrency') || 'USD', + defaultLanguage: this.configService.get('booking.defaultLanguage') || 'es', + }; + + this.logger.log('Booking.com service initialized'); + this.logger.log('Environment: ' + this.config.environment); + this.logger.log('Configured: ' + (this.isConfigured() ? 'Yes' : 'No - add credentials to .env')); + } + + private isConfigured(): boolean { + return !!(this.config.affiliateId && this.config.apiKey); + } + + private getAuthHeaders(): Record { + return { + 'Authorization': 'Basic ' + Buffer.from(this.config.affiliateId + ':' + this.config.apiKey).toString('base64'), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-Affiliate-Id': this.config.affiliateId, + }; + } + + private async makeRequest(endpoint: string, method: string = 'GET', body?: any): Promise { + if (!this.isConfigured()) { + throw new HttpException( + 'Booking.com API not configured. Add BOOKING_AFFILIATE_ID and BOOKING_API_KEY to .env', + HttpStatus.SERVICE_UNAVAILABLE, + ); + } + + const url = this.config.baseUrl + endpoint; + this.logger.debug('Making request to: ' + url); + + try { + const options: RequestInit = { + method, + headers: this.getAuthHeaders(), + }; + + if (body && method !== 'GET') { + options.body = JSON.stringify(body); + } + + const response = await fetch(url, options); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + this.logger.error('Booking.com API error: ' + JSON.stringify(errorData)); + throw new HttpException( + errorData.message || 'Booking.com API request failed', + response.status, + ); + } + + return response.json(); + } catch (error) { + if (error instanceof HttpException) throw error; + this.logger.error('Booking.com request error: ' + error.message); + throw new HttpException( + 'Failed to communicate with Booking.com API', + HttpStatus.SERVICE_UNAVAILABLE, + ); + } + } + + // ==================== ACCOMMODATIONS ==================== + + async searchAccommodations(dto: SearchAccommodationsDto): Promise { + this.logger.log('Searching accommodations in: ' + dto.city); + + const params: any = { + city: dto.city, + checkin: dto.checkin, + checkout: dto.checkout, + guest_qty: dto.adults || 2, + room_qty: dto.rooms || 1, + currency: dto.currency || this.config.defaultCurrency, + language: this.config.defaultLanguage, + rows: dto.limit || 20, + page: dto.page || 1, + }; + + if (dto.countryCode) params.country = dto.countryCode; + if (dto.children) params.children_qty = dto.children; + if (dto.minStars) params.min_review_score = dto.minStars * 2; // Booking uses 1-10 scale + if (dto.latitude && dto.longitude) { + params.latitude = dto.latitude; + params.longitude = dto.longitude; + if (dto.radius) params.radius = dto.radius; + } + if (dto.sortBy) params.order_by = dto.sortBy; + + const queryString = new URLSearchParams(params).toString(); + return this.makeRequest('/accommodations/search?' + queryString); + } + + async getAccommodationDetails(hotelId: string): Promise { + this.logger.log('Getting accommodation details: ' + hotelId); + return this.makeRequest('/accommodations/' + hotelId); + } + + async checkAvailability(hotelId: string, checkin: string, checkout: string, guests: number): Promise { + this.logger.log('Checking availability for: ' + hotelId); + const params = new URLSearchParams({ + checkin, + checkout, + guest_qty: guests.toString(), + currency: this.config.defaultCurrency, + }); + return this.makeRequest('/accommodations/' + hotelId + '/availability?' + params); + } + + // ==================== FLIGHTS ==================== + + async searchFlights(dto: SearchFlightsDto): Promise { + this.logger.log('Searching flights: ' + dto.origin + ' -> ' + dto.destination); + + const body: any = { + origin: dto.origin.toUpperCase(), + destination: dto.destination.toUpperCase(), + departure_date: dto.departureDate, + adults: dto.adults || 1, + currency: dto.currency || this.config.defaultCurrency, + }; + + if (dto.returnDate) body.return_date = dto.returnDate; + if (dto.children) body.children = dto.children; + if (dto.infants) body.infants = dto.infants; + if (dto.cabinClass) body.cabin_class = dto.cabinClass; + if (dto.directOnly) body.direct_only = dto.directOnly; + if (dto.tripType === TripType.ONEWAY) body.trip_type = 'oneway'; + + return this.makeRequest('/flights/search', 'POST', body); + } + + async getFlightDetails(flightId: string): Promise { + this.logger.log('Getting flight details: ' + flightId); + return this.makeRequest('/flights/' + flightId); + } + + // ==================== CAR RENTALS ==================== + + async searchCars(dto: SearchCarsDto): Promise { + this.logger.log('Searching cars at: ' + dto.pickupLocation); + + const params: any = { + pickup_location: dto.pickupLocation.toUpperCase(), + dropoff_location: (dto.dropoffLocation || dto.pickupLocation).toUpperCase(), + pickup_date: dto.pickupDate, + pickup_time: dto.pickupTime, + dropoff_date: dto.dropoffDate, + dropoff_time: dto.dropoffTime, + currency: dto.currency || this.config.defaultCurrency, + driver_age: dto.driverAge || 30, + }; + + if (dto.category) params.car_type = dto.category; + if (dto.transmission) params.transmission = dto.transmission; + if (dto.airConditioning) params.air_conditioning = dto.airConditioning; + if (dto.sortBy) params.order_by = dto.sortBy; + if (dto.limit) params.rows = dto.limit; + + const queryString = new URLSearchParams(params).toString(); + return this.makeRequest('/car-rentals/search?' + queryString); + } + + async getCarDetails(carId: string): Promise { + this.logger.log('Getting car details: ' + carId); + return this.makeRequest('/car-rentals/' + carId); + } + + async getCarDepots(locationCode: string): Promise { + this.logger.log('Getting car depots for: ' + locationCode); + return this.makeRequest('/car-rentals/depots?location=' + locationCode); + } + + // ==================== LOCATIONS ==================== + + async searchLocations(query: string, type?: 'city' | 'airport' | 'hotel'): Promise { + this.logger.log('Searching locations: ' + query); + const params: any = { text: query, language: this.config.defaultLanguage }; + if (type) params.type = type; + const queryString = new URLSearchParams(params).toString(); + return this.makeRequest('/locations/search?' + queryString); + } + + // ==================== STATUS ==================== + + getStatus(): any { + return { + provider: 'Booking.com', + environment: this.config.environment, + baseUrl: this.config.baseUrl, + defaultCurrency: this.config.defaultCurrency, + defaultLanguage: this.config.defaultLanguage, + isConfigured: this.isConfigured(), + services: { + accommodations: true, + flights: true, + carRentals: true, + taxis: true, + }, + message: this.isConfigured() + ? 'Booking.com API ready' + : 'Add BOOKING_AFFILIATE_ID and BOOKING_API_KEY to .env', + }; + } +} diff --git a/src/modules/booking/dto/search-accommodations.dto.ts b/src/modules/booking/dto/search-accommodations.dto.ts new file mode 100644 index 0000000..a998867 --- /dev/null +++ b/src/modules/booking/dto/search-accommodations.dto.ts @@ -0,0 +1,97 @@ +import { IsString, IsDateString, IsOptional, IsInt, Min, Max, IsNumber, IsArray, IsEnum } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +export class SearchAccommodationsDto { + @ApiProperty({ description: 'City or location name', example: 'Santo Domingo' }) + @IsString() + city: string; + + @ApiPropertyOptional({ description: 'Country code (ISO 3166-1 alpha-2)', example: 'DO' }) + @IsOptional() + @IsString() + countryCode?: string; + + @ApiProperty({ description: 'Check-in date (YYYY-MM-DD)', example: '2025-06-15' }) + @IsDateString() + checkin: string; + + @ApiProperty({ description: 'Check-out date (YYYY-MM-DD)', example: '2025-06-22' }) + @IsDateString() + checkout: string; + + @ApiProperty({ description: 'Number of adults', example: 2, default: 2 }) + @IsInt() + @Min(1) + @Max(30) + adults: number; + + @ApiPropertyOptional({ description: 'Number of children', example: 0 }) + @IsOptional() + @IsInt() + @Min(0) + @Max(10) + children?: number; + + @ApiPropertyOptional({ description: 'Number of rooms', example: 1 }) + @IsOptional() + @IsInt() + @Min(1) + @Max(30) + rooms?: number; + + @ApiPropertyOptional({ description: 'Currency code', example: 'USD' }) + @IsOptional() + @IsString() + currency?: string; + + @ApiPropertyOptional({ description: 'Minimum star rating (1-5)', example: 3 }) + @IsOptional() + @IsInt() + @Min(1) + @Max(5) + minStars?: number; + + @ApiPropertyOptional({ description: 'Maximum price per night', example: 200 }) + @IsOptional() + @IsNumber() + @Type(() => Number) + maxPrice?: number; + + @ApiPropertyOptional({ description: 'Latitude for geo search', example: 18.4861 }) + @IsOptional() + @IsNumber() + @Type(() => Number) + latitude?: number; + + @ApiPropertyOptional({ description: 'Longitude for geo search', example: -69.9312 }) + @IsOptional() + @IsNumber() + @Type(() => Number) + longitude?: number; + + @ApiPropertyOptional({ description: 'Search radius in km', example: 10 }) + @IsOptional() + @IsInt() + @Min(1) + @Max(50) + radius?: number; + + @ApiPropertyOptional({ description: 'Sort by: price, popularity, rating, distance', example: 'price' }) + @IsOptional() + @IsString() + sortBy?: string; + + @ApiPropertyOptional({ description: 'Number of results (max 100)', example: 20 }) + @IsOptional() + @IsInt() + @Min(1) + @Max(100) + limit?: number; + + @ApiPropertyOptional({ description: 'Page number for pagination', example: 1 }) + @IsOptional() + @IsInt() + @Min(1) + page?: number; +} diff --git a/src/modules/booking/dto/search-cars.dto.ts b/src/modules/booking/dto/search-cars.dto.ts new file mode 100644 index 0000000..9ea60e3 --- /dev/null +++ b/src/modules/booking/dto/search-cars.dto.ts @@ -0,0 +1,92 @@ +import { IsString, IsDateString, IsOptional, IsInt, Min, IsEnum, IsBoolean, IsNumber, Max } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +export enum CarCategory { + MINI = 'MINI', + ECONOMY = 'ECONOMY', + COMPACT = 'COMPACT', + MIDSIZE = 'MIDSIZE', + STANDARD = 'STANDARD', + FULLSIZE = 'FULLSIZE', + PREMIUM = 'PREMIUM', + LUXURY = 'LUXURY', + SUV = 'SUV', + VAN = 'VAN', +} + +export enum TransmissionType { + AUTOMATIC = 'AUTOMATIC', + MANUAL = 'MANUAL', +} + +export class SearchCarsDto { + @ApiProperty({ description: 'Pick-up location (city or airport code)', example: 'SDQ' }) + @IsString() + pickupLocation: string; + + @ApiPropertyOptional({ description: 'Drop-off location if different', example: 'PUJ' }) + @IsOptional() + @IsString() + dropoffLocation?: string; + + @ApiProperty({ description: 'Pick-up date (YYYY-MM-DD)', example: '2025-06-15' }) + @IsDateString() + pickupDate: string; + + @ApiProperty({ description: 'Pick-up time (HH:MM)', example: '10:00' }) + @IsString() + pickupTime: string; + + @ApiProperty({ description: 'Drop-off date (YYYY-MM-DD)', example: '2025-06-22' }) + @IsDateString() + dropoffDate: string; + + @ApiProperty({ description: 'Drop-off time (HH:MM)', example: '10:00' }) + @IsString() + dropoffTime: string; + + @ApiPropertyOptional({ description: 'Currency for prices', example: 'USD' }) + @IsOptional() + @IsString() + currency?: string; + + @ApiPropertyOptional({ description: 'Driver age', example: 30 }) + @IsOptional() + @IsInt() + @Min(18) + driverAge?: number; + + @ApiPropertyOptional({ description: 'Car category', enum: CarCategory }) + @IsOptional() + @IsEnum(CarCategory) + category?: CarCategory; + + @ApiPropertyOptional({ description: 'Transmission type', enum: TransmissionType }) + @IsOptional() + @IsEnum(TransmissionType) + transmission?: TransmissionType; + + @ApiPropertyOptional({ description: 'Air conditioning required', example: true }) + @IsOptional() + @IsBoolean() + airConditioning?: boolean; + + @ApiPropertyOptional({ description: 'Max price per day', example: 100 }) + @IsOptional() + @IsNumber() + @Type(() => Number) + maxPricePerDay?: number; + + @ApiPropertyOptional({ description: 'Sort by: price, rating', example: 'price' }) + @IsOptional() + @IsString() + sortBy?: string; + + @ApiPropertyOptional({ description: 'Max results', example: 20 }) + @IsOptional() + @IsInt() + @Min(1) + @Max(50) + limit?: number; +} diff --git a/src/modules/booking/dto/search-flights.dto.ts b/src/modules/booking/dto/search-flights.dto.ts new file mode 100644 index 0000000..a8a1561 --- /dev/null +++ b/src/modules/booking/dto/search-flights.dto.ts @@ -0,0 +1,80 @@ +import { IsString, IsDateString, IsOptional, IsInt, Min, Max, IsEnum, IsBoolean } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum CabinClass { + ECONOMY = 'ECONOMY', + PREMIUM_ECONOMY = 'PREMIUM_ECONOMY', + BUSINESS = 'BUSINESS', + FIRST = 'FIRST', +} + +export enum TripType { + ONEWAY = 'ONEWAY', + ROUNDTRIP = 'ROUNDTRIP', +} + +export class SearchFlightsDto { + @ApiProperty({ description: 'Origin airport IATA code', example: 'SDQ' }) + @IsString() + origin: string; + + @ApiProperty({ description: 'Destination airport IATA code', example: 'MIA' }) + @IsString() + destination: string; + + @ApiProperty({ description: 'Departure date (YYYY-MM-DD)', example: '2025-06-15' }) + @IsDateString() + departureDate: string; + + @ApiPropertyOptional({ description: 'Return date for round trip (YYYY-MM-DD)', example: '2025-06-22' }) + @IsOptional() + @IsDateString() + returnDate?: string; + + @ApiProperty({ description: 'Trip type', enum: TripType, default: TripType.ROUNDTRIP }) + @IsOptional() + @IsEnum(TripType) + tripType?: TripType; + + @ApiProperty({ description: 'Number of adults', example: 1, default: 1 }) + @IsInt() + @Min(1) + @Max(9) + adults: number; + + @ApiPropertyOptional({ description: 'Number of children (2-11)', example: 0 }) + @IsOptional() + @IsInt() + @Min(0) + @Max(8) + children?: number; + + @ApiPropertyOptional({ description: 'Number of infants (0-2)', example: 0 }) + @IsOptional() + @IsInt() + @Min(0) + @Max(4) + infants?: number; + + @ApiPropertyOptional({ description: 'Cabin class', enum: CabinClass }) + @IsOptional() + @IsEnum(CabinClass) + cabinClass?: CabinClass; + + @ApiPropertyOptional({ description: 'Direct flights only', example: false }) + @IsOptional() + @IsBoolean() + directOnly?: boolean; + + @ApiPropertyOptional({ description: 'Currency for prices', example: 'USD' }) + @IsOptional() + @IsString() + currency?: string; + + @ApiPropertyOptional({ description: 'Max results', example: 20 }) + @IsOptional() + @IsInt() + @Min(1) + @Max(100) + limit?: number; +} diff --git a/src/modules/booking/index.ts b/src/modules/booking/index.ts new file mode 100644 index 0000000..8aa9bea --- /dev/null +++ b/src/modules/booking/index.ts @@ -0,0 +1,6 @@ +export * from './booking.module'; +export * from './booking.service'; +export * from './booking.controller'; +export * from './dto/search-accommodations.dto'; +export * from './dto/search-flights.dto'; +export * from './dto/search-cars.dto'; diff --git a/src/modules/collections/collections.controller.ts b/src/modules/collections/collections.controller.ts new file mode 100644 index 0000000..e4a7a67 --- /dev/null +++ b/src/modules/collections/collections.controller.ts @@ -0,0 +1,125 @@ +import { + Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, Request, +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam, +} from '@nestjs/swagger'; +import { CollectionsService } from './collections.service'; +import { CreateCollectionDto, AddCollectionItemDto } from './dto/create-collection.dto'; +import { UpdateCollectionDto } from './dto/update-collection.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { UserCollection, CollectionItem } from '../../entities/user-collection.entity'; + +@ApiTags('Collections') +@Controller('collections') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class CollectionsController { + constructor(private readonly collectionsService: CollectionsService) {} + + @Post() + @ApiOperation({ summary: 'Create a new collection' }) + @ApiResponse({ status: 201, description: 'Collection created', type: UserCollection }) + @ApiResponse({ status: 409, description: 'Collection name already exists' }) + create(@Body() createDto: CreateCollectionDto, @Request() req) { + return this.collectionsService.create(req.user.id, createDto); + } + + @Get('my') + @ApiOperation({ summary: 'Get my collections' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiResponse({ status: 200, description: 'User collections' }) + findMyCollections( + @Request() req, + @Query('page') page?: number, + @Query('limit') limit?: number, + ) { + return this.collectionsService.findAllByUser(req.user.id, false, page, limit); + } + + @Get('my/stats') + @ApiOperation({ summary: 'Get collection statistics' }) + @ApiResponse({ status: 200, description: 'Collection stats' }) + getStats(@Request() req) { + return this.collectionsService.getCollectionStats(req.user.id); + } + + @Get(':id') + @ApiOperation({ summary: 'Get collection by ID' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, type: UserCollection }) + @ApiResponse({ status: 404, description: 'Collection not found' }) + findOne(@Param('id') id: string, @Request() req) { + return this.collectionsService.findOne(req.user.id, id); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update collection' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, type: UserCollection }) + update( + @Param('id') id: string, + @Body() updateDto: UpdateCollectionDto, + @Request() req, + ) { + return this.collectionsService.update(req.user.id, id, updateDto); + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete collection' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, description: 'Collection deleted' }) + remove(@Param('id') id: string, @Request() req) { + return this.collectionsService.remove(req.user.id, id); + } + + // Collection Items + @Post(':id/items') + @ApiOperation({ summary: 'Add item to collection' }) + @ApiParam({ name: 'id', type: 'string', description: 'Collection ID' }) + @ApiResponse({ status: 201, type: CollectionItem }) + @ApiResponse({ status: 409, description: 'Item already in collection' }) + addItem( + @Param('id') id: string, + @Body() addItemDto: AddCollectionItemDto, + @Request() req, + ) { + return this.collectionsService.addItem(req.user.id, id, addItemDto); + } + + @Delete(':id/items/:itemId') + @ApiOperation({ summary: 'Remove item from collection' }) + @ApiParam({ name: 'id', type: 'string', description: 'Collection ID' }) + @ApiParam({ name: 'itemId', type: 'string', description: 'Item ID' }) + @ApiResponse({ status: 200, description: 'Item removed' }) + removeItem( + @Param('id') id: string, + @Param('itemId') itemId: string, + @Request() req, + ) { + return this.collectionsService.removeItem(req.user.id, id, itemId); + } + + @Patch(':id/items/order') + @ApiOperation({ summary: 'Reorder items in collection' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, description: 'Order updated' }) + updateItemOrder( + @Param('id') id: string, + @Body() body: { itemIds: string[] }, + @Request() req, + ) { + return this.collectionsService.updateItemOrder(req.user.id, id, body.itemIds); + } + + @Patch('order') + @ApiOperation({ summary: 'Reorder collections' }) + @ApiResponse({ status: 200, description: 'Order updated' }) + updateCollectionOrder( + @Body() body: { collectionIds: string[] }, + @Request() req, + ) { + return this.collectionsService.updateCollectionOrder(req.user.id, body.collectionIds); + } +} diff --git a/src/modules/collections/collections.module.ts b/src/modules/collections/collections.module.ts new file mode 100644 index 0000000..42d0ffd --- /dev/null +++ b/src/modules/collections/collections.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CollectionsService } from './collections.service'; +import { CollectionsController } from './collections.controller'; +import { UserCollection, CollectionItem } from '../../entities/user-collection.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([UserCollection, CollectionItem])], + controllers: [CollectionsController], + providers: [CollectionsService], + exports: [CollectionsService], +}) +export class CollectionsModule {} diff --git a/src/modules/collections/collections.service.ts b/src/modules/collections/collections.service.ts new file mode 100644 index 0000000..dfc2b01 --- /dev/null +++ b/src/modules/collections/collections.service.ts @@ -0,0 +1,205 @@ +import { Injectable, NotFoundException, ConflictException, ForbiddenException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { UserCollection, CollectionItem } from '../../entities/user-collection.entity'; +import { CreateCollectionDto, AddCollectionItemDto } from './dto/create-collection.dto'; +import { UpdateCollectionDto } from './dto/update-collection.dto'; + +@Injectable() +export class CollectionsService { + constructor( + @InjectRepository(UserCollection) + private readonly collectionRepository: Repository, + @InjectRepository(CollectionItem) + private readonly itemRepository: Repository, + ) {} + + async create(userId: string, createDto: CreateCollectionDto): Promise { + // Check for duplicate name + const existing = await this.collectionRepository.findOne({ + where: { userId, name: createDto.name }, + }); + + if (existing) { + throw new ConflictException('Collection with this name already exists'); + } + + // Get max sort order + const maxOrder = await this.collectionRepository + .createQueryBuilder('c') + .select('MAX(c.sort_order)', 'max') + .where('c.user_id = :userId', { userId }) + .getRawOne(); + + const collection = this.collectionRepository.create({ + ...createDto, + userId, + sortOrder: (maxOrder?.max || 0) + 1, + }); + + return this.collectionRepository.save(collection); + } + + async findAllByUser( + userId: string, + includePublic = false, + page = 1, + limit = 20, + ): Promise<{ data: UserCollection[]; total: number }> { + const queryBuilder = this.collectionRepository + .createQueryBuilder('c') + .leftJoinAndSelect('c.items', 'items') + .where('c.user_id = :userId', { userId }); + + if (includePublic) { + queryBuilder.orWhere('c.is_public = true'); + } + + const [data, total] = await queryBuilder + .orderBy('c.sort_order', 'ASC') + .skip((page - 1) * limit) + .take(limit) + .getManyAndCount(); + + return { data, total }; + } + + async findOne(userId: string, id: string): Promise { + const collection = await this.collectionRepository.findOne({ + where: { id }, + relations: ['items'], + }); + + if (!collection) { + throw new NotFoundException('Collection not found'); + } + + // Check ownership or public access + if (collection.userId !== userId && !collection.isPublic) { + throw new ForbiddenException('Access denied'); + } + + return collection; + } + + async update(userId: string, id: string, updateDto: UpdateCollectionDto): Promise { + const collection = await this.collectionRepository.findOne({ + where: { id, userId }, + }); + + if (!collection) { + throw new NotFoundException('Collection not found'); + } + + Object.assign(collection, updateDto); + return this.collectionRepository.save(collection); + } + + async remove(userId: string, id: string): Promise { + const collection = await this.collectionRepository.findOne({ + where: { id, userId }, + }); + + if (!collection) { + throw new NotFoundException('Collection not found'); + } + + await this.collectionRepository.remove(collection); + } + + // Collection Items + async addItem(userId: string, collectionId: string, addItemDto: AddCollectionItemDto): Promise { + const collection = await this.collectionRepository.findOne({ + where: { id: collectionId, userId }, + }); + + if (!collection) { + throw new NotFoundException('Collection not found'); + } + + // Check if item already in collection + const existing = await this.itemRepository.findOne({ + where: { + collectionId, + itemId: addItemDto.itemId, + itemType: addItemDto.itemType, + }, + }); + + if (existing) { + throw new ConflictException('Item already in collection'); + } + + // Get max sort order + const maxOrder = await this.itemRepository + .createQueryBuilder('i') + .select('MAX(i.sort_order)', 'max') + .where('i.collection_id = :collectionId', { collectionId }) + .getRawOne(); + + const item = this.itemRepository.create({ + ...addItemDto, + collectionId, + sortOrder: (maxOrder?.max || 0) + 1, + }); + + return this.itemRepository.save(item); + } + + async removeItem(userId: string, collectionId: string, itemId: string): Promise { + const collection = await this.collectionRepository.findOne({ + where: { id: collectionId, userId }, + }); + + if (!collection) { + throw new NotFoundException('Collection not found'); + } + + const item = await this.itemRepository.findOne({ + where: { id: itemId, collectionId }, + }); + + if (!item) { + throw new NotFoundException('Item not found in collection'); + } + + await this.itemRepository.remove(item); + } + + async updateItemOrder(userId: string, collectionId: string, itemIds: string[]): Promise { + const collection = await this.collectionRepository.findOne({ + where: { id: collectionId, userId }, + }); + + if (!collection) { + throw new NotFoundException('Collection not found'); + } + + await Promise.all( + itemIds.map((id, index) => + this.itemRepository.update({ id, collectionId }, { sortOrder: index }) + ) + ); + } + + async updateCollectionOrder(userId: string, collectionIds: string[]): Promise { + await Promise.all( + collectionIds.map((id, index) => + this.collectionRepository.update({ id, userId }, { sortOrder: index }) + ) + ); + } + + async getCollectionStats(userId: string): Promise<{ total: number; publicCount: number; itemsCount: number }> { + const collections = await this.collectionRepository.find({ + where: { userId }, + relations: ['items'], + }); + + return { + total: collections.length, + publicCount: collections.filter(c => c.isPublic).length, + itemsCount: collections.reduce((sum, c) => sum + (c.items?.length || 0), 0), + }; + } +} diff --git a/src/modules/collections/dto/create-collection.dto.ts b/src/modules/collections/dto/create-collection.dto.ts new file mode 100644 index 0000000..08ae4b6 --- /dev/null +++ b/src/modules/collections/dto/create-collection.dto.ts @@ -0,0 +1,59 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsString, IsOptional, IsBoolean, MaxLength, IsHexColor } from 'class-validator'; + +export class CreateCollectionDto { + @ApiProperty({ description: 'Collection name', example: 'My Beach Trip' }) + @IsString() + @MaxLength(100) + name: string; + + @ApiPropertyOptional({ description: 'Collection description' }) + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional({ description: 'Cover image URL' }) + @IsOptional() + @IsString() + coverImageUrl?: string; + + @ApiPropertyOptional({ description: 'Collection color theme', example: '#FF5722' }) + @IsOptional() + @IsString() + color?: string; + + @ApiPropertyOptional({ description: 'Collection icon', example: 'beach' }) + @IsOptional() + @IsString() + icon?: string; + + @ApiPropertyOptional({ description: 'Is public collection', default: false }) + @IsOptional() + @IsBoolean() + isPublic?: boolean; +} + +export class AddCollectionItemDto { + @ApiProperty({ description: 'Item ID' }) + @IsString() + itemId: string; + + @ApiProperty({ description: 'Item type', example: 'place' }) + @IsString() + itemType: string; + + @ApiPropertyOptional({ description: 'Item name' }) + @IsOptional() + @IsString() + itemName?: string; + + @ApiPropertyOptional({ description: 'Item image URL' }) + @IsOptional() + @IsString() + itemImageUrl?: string; + + @ApiPropertyOptional({ description: 'Notes' }) + @IsOptional() + @IsString() + notes?: string; +} diff --git a/src/modules/collections/dto/index.ts b/src/modules/collections/dto/index.ts new file mode 100644 index 0000000..8dde214 --- /dev/null +++ b/src/modules/collections/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-collection.dto'; +export * from './update-collection.dto'; diff --git a/src/modules/collections/dto/update-collection.dto.ts b/src/modules/collections/dto/update-collection.dto.ts new file mode 100644 index 0000000..89a18cf --- /dev/null +++ b/src/modules/collections/dto/update-collection.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateCollectionDto } from './create-collection.dto'; + +export class UpdateCollectionDto extends PartialType(CreateCollectionDto) {} diff --git a/src/modules/content-generator/content-generator.controller.ts b/src/modules/content-generator/content-generator.controller.ts new file mode 100644 index 0000000..3486d3a --- /dev/null +++ b/src/modules/content-generator/content-generator.controller.ts @@ -0,0 +1,100 @@ +import { + Controller, + Post, + Get, + Query, + UseGuards, + Body, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiQuery, +} from '@nestjs/swagger'; +import { ContentGeneratorService, GenerationResult } from './content-generator.service'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { Roles } from '../../common/decorators/roles.decorator'; + +@ApiTags('Content Generator') +@Controller('content-generator') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('admin') +@ApiBearerAuth('JWT-auth') +export class ContentGeneratorController { + constructor(private readonly contentGeneratorService: ContentGeneratorService) {} + + @Get('stats') + @ApiOperation({ summary: 'Get content generation statistics' }) + @ApiResponse({ status: 200, description: 'Statistics retrieved successfully' }) + async getStats() { + return this.contentGeneratorService.getStats(); + } + + @Post('generate-descriptions') + @ApiOperation({ summary: 'Generate descriptions for monuments (Admin only)' }) + @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Max places to process (default: 10)' }) + @ApiQuery({ name: 'languages', required: false, type: String, description: 'Comma-separated language codes (default: es,en,fr,it,de)' }) + @ApiQuery({ name: 'onlyMissing', required: false, type: Boolean, description: 'Only process places without description (default: true)' }) + @ApiResponse({ status: 200, description: 'Descriptions generated successfully' }) + async generateDescriptions( + @Query('limit') limit?: number, + @Query('languages') languages?: string, + @Query('onlyMissing') onlyMissing?: boolean, + ) { + const languageList = languages ? languages.split(',').map(l => l.trim()) : undefined; + + const results = await this.contentGeneratorService.generateAllDescriptions({ + limit: limit || 10, + languages: languageList, + onlyMissing: onlyMissing !== false, + }); + + return { + success: true, + processed: results.length, + successful: results.filter(r => r.success).length, + failed: results.filter(r => !r.success).length, + results, + }; + } + + @Post('generate-audios') + @ApiOperation({ summary: 'Generate audio files for monuments (Admin only)' }) + @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Max places to process (default: 10)' }) + @ApiQuery({ name: 'languages', required: false, type: String, description: 'Comma-separated language codes (default: es,en,fr,it,de)' }) + @ApiResponse({ status: 200, description: 'Audios generated successfully' }) + async generateAudios( + @Query('limit') limit?: number, + @Query('languages') languages?: string, + ) { + const languageList = languages ? languages.split(',').map(l => l.trim()) : undefined; + + const results = await this.contentGeneratorService.generateAllAudios({ + limit: limit || 10, + languages: languageList, + }); + + return { + success: true, + processed: results.length, + successful: results.filter(r => r.success).length, + failed: results.filter(r => !r.success).length, + results, + }; + } + + @Post('generate-single') + @ApiOperation({ summary: 'Generate content for a single place' }) + async generateSingle( + @Body() body: { placeId: number; languages?: string[] }, + ) { + // This would be implemented to generate content for a specific place + return { + message: 'Single place generation not yet implemented', + placeId: body.placeId, + }; + } +} diff --git a/src/modules/content-generator/content-generator.module.ts b/src/modules/content-generator/content-generator.module.ts new file mode 100644 index 0000000..3e90e40 --- /dev/null +++ b/src/modules/content-generator/content-generator.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ContentGeneratorService } from './content-generator.service'; +import { ContentGeneratorController } from './content-generator.controller'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { KimiModule } from '../kimi/kimi.module'; +import { AIGuideModule } from '../ai-guide/ai-guide.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([PlaceOfInterest]), + KimiModule, + AIGuideModule, + ], + controllers: [ContentGeneratorController], + providers: [ContentGeneratorService], + exports: [ContentGeneratorService], +}) +export class ContentGeneratorModule {} diff --git a/src/modules/content-generator/content-generator.service.ts b/src/modules/content-generator/content-generator.service.ts new file mode 100644 index 0000000..10a9156 --- /dev/null +++ b/src/modules/content-generator/content-generator.service.ts @@ -0,0 +1,287 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { KimiService } from '../kimi/kimi.service'; +import { TTSService } from '../ai-guide/tts.service'; + +export interface GenerationResult { + placeId: string; + placeName: string; + language: string; + success: boolean; + audioUrl?: string; + error?: string; +} + +@Injectable() +export class ContentGeneratorService { + private readonly logger = new Logger(ContentGeneratorService.name); + private readonly languages = ['es', 'en', 'fr', 'it', 'de']; + + constructor( + @InjectRepository(PlaceOfInterest) + private readonly placeRepository: Repository, + private readonly kimiService: KimiService, + private readonly ttsService: TTSService, + ) {} + + private getPrompt(placeName: string, country: string, language: string): string { + const countryName = country === 'DO' ? 'República Dominicana' : 'Puerto Rico'; + const countryNameEn = country === 'DO' ? 'Dominican Republic' : 'Puerto Rico'; + + const prompts: Record = { + es: `Actúa como un guía turístico ${country === 'DO' ? 'dominicano' : 'puertorriqueño'} experto. Dame una descripción completa e interesante de ${placeName} en ${countryName}. Incluye su historia, importancia cultural, arquitectura, datos curiosos, horarios de visita si aplica. Habla en segunda persona directamente al turista usando "tú" (NO uses "vosotros" ni expresiones de España). Usa español latinoamericano natural. Máximo 250 palabras.`, + + en: `Act as an expert ${country === 'DO' ? 'Dominican' : 'Puerto Rican'} tour guide. Give me a complete and interesting description of ${placeName} in ${countryNameEn}. Include its history, cultural importance, architecture, fun facts, and visiting hours if applicable. Speak in second person as if you were giving a tour. Maximum 250 words.`, + + fr: `Agis comme un guide touristique ${country === 'DO' ? 'dominicain' : 'portoricain'} expert. Donne-moi une description complète et intéressante de ${placeName} en ${countryName}. Inclus son histoire, son importance culturelle, son architecture, des anecdotes et les horaires de visite si applicable. Parle à la deuxième personne comme si tu donnais une visite guidée. Maximum 250 mots.`, + + it: `Agisci come una guida turistica ${country === 'DO' ? 'dominicana' : 'portoricana'} esperta. Dammi una descrizione completa e interessante di ${placeName} in ${countryName}. Includi la sua storia, importanza culturale, architettura, curiosità e orari di visita se applicabile. Parla in seconda persona come se stessi facendo un tour. Massimo 250 parole.`, + + de: `Handle als erfahrener ${country === 'DO' ? 'dominikanischer' : 'puertoricanischer'} Reiseführer. Gib mir eine vollständige und interessante Beschreibung von ${placeName} in ${countryName}. Füge die Geschichte, kulturelle Bedeutung, Architektur, interessante Fakten und Besuchszeiten falls zutreffend hinzu. Sprich in der zweiten Person, als würdest du eine Tour geben. Maximal 250 Wörter.`, + }; + + return prompts[language] || prompts['en']; + } + + private getLanguageName(code: string): string { + const names: Record = { + es: 'español latinoamericano', + en: 'English', + fr: 'français', + it: 'italiano', + de: 'Deutsch', + }; + return names[code] || 'English'; + } + + async generateDescription(place: PlaceOfInterest, language: string): Promise { + const country = place.country || 'DO'; + const prompt = this.getPrompt(place.name, country, language); + + try { + const response = await this.kimiService.chat([ + { + role: 'system', + content: `Eres un guía turístico experto del Caribe. Responde siempre en ${this.getLanguageName(language)}. Sé informativo, amigable y entusiasta. No uses emojis.`, + }, + { + role: 'user', + content: prompt, + }, + ], { + temperature: 0.7, + maxTokens: 1000, + }); + + return response; + } catch (error: any) { + this.logger.error(`Error generando descripción para ${place.name} (${language}): ${error.message}`); + return null; + } + } + + async generateAllDescriptions(options?: { + limit?: number; + languages?: string[]; + onlyMissing?: boolean; + }): Promise { + const limit = options?.limit || 10; + const languages = options?.languages || this.languages; + const onlyMissing = options?.onlyMissing !== false; + + this.logger.log(`Iniciando generación de descripciones...`); + this.logger.log(`Límite: ${limit} lugares, Idiomas: ${languages.join(', ')}`); + + let places: PlaceOfInterest[]; + + if (onlyMissing) { + places = await this.placeRepository.find({ + where: { active: true }, + take: limit, + }); + places = places.filter(p => !p.descriptionEs); + } else { + places = await this.placeRepository.find({ + where: { active: true }, + take: limit, + }); + } + + this.logger.log(`Encontrados ${places.length} lugares para procesar`); + + const results: GenerationResult[] = []; + let processed = 0; + const total = places.length * languages.length; + + for (const place of places) { + for (const lang of languages) { + processed++; + this.logger.log(`[${processed}/${total}] Generando ${lang} para: ${place.name}`); + + const description = await this.generateDescription(place, lang); + + if (description) { + const propName = `description${lang.charAt(0).toUpperCase() + lang.slice(1)}` as keyof PlaceOfInterest; + await this.placeRepository + .createQueryBuilder() + .update() + .set({ [propName]: description } as any) + .where('id = :id', { id: place.id }) + .execute(); + + results.push({ + placeId: place.id, + placeName: place.name, + language: lang, + success: true, + }); + + this.logger.log(`OK: ${place.name} (${lang})`); + } else { + results.push({ + placeId: place.id, + placeName: place.name, + language: lang, + success: false, + error: 'No response from Kimi', + }); + + this.logger.warn(`FAILED: ${place.name} (${lang})`); + } + + await this.delay(500); + } + } + + const successful = results.filter(r => r.success).length; + this.logger.log(`Generación completada: ${successful}/${total} exitosos`); + + return results; + } + + async generateAllAudios(options?: { + limit?: number; + languages?: string[]; + }): Promise { + const limit = options?.limit || 100; + const languages = options?.languages || this.languages; + + this.logger.log(`Iniciando generación de audios...`); + this.logger.log(`Límite: ${limit} lugares, Idiomas: ${languages.join(', ')}`); + + if (!this.ttsService.isAvailable()) { + this.logger.error('TTS Service (Piper) no está disponible'); + return []; + } + + const places = await this.placeRepository.find({ + where: { active: true }, + take: limit, + }); + + this.logger.log(`Encontrados ${places.length} lugares`); + + const results: GenerationResult[] = []; + let processed = 0; + let skipped = 0; + + for (const place of places) { + for (const lang of languages) { + const descProp = `description${lang.charAt(0).toUpperCase() + lang.slice(1)}` as keyof PlaceOfInterest; + const audioProp = `audioUrl${lang.charAt(0).toUpperCase() + lang.slice(1)}` as keyof PlaceOfInterest; + + const description = place[descProp] as string; + const existingAudio = place[audioProp] as string; + + if (!description) { + skipped++; + continue; + } + + if (existingAudio) { + skipped++; + continue; + } + + processed++; + this.logger.log(`[${processed}] Generando audio ${lang} para: ${place.name}`); + + try { + const audioUrl = await this.ttsService.generateSpeech(description, lang); + + if (audioUrl) { + await this.placeRepository + .createQueryBuilder() + .update() + .set({ [audioProp]: audioUrl } as any) + .where('id = :id', { id: place.id }) + .execute(); + + results.push({ + placeId: place.id, + placeName: place.name, + language: lang, + success: true, + audioUrl, + }); + + this.logger.log(`OK: Audio ${place.name} (${lang})`); + } else { + results.push({ + placeId: place.id, + placeName: place.name, + language: lang, + success: false, + error: 'TTS generation returned null', + }); + + this.logger.warn(`FAILED: Audio ${place.name} (${lang})`); + } + } catch (error: any) { + results.push({ + placeId: place.id, + placeName: place.name, + language: lang, + success: false, + error: error.message, + }); + + this.logger.error(`ERROR: Audio ${place.name} (${lang}): ${error.message}`); + } + + await this.delay(100); + } + } + + const successful = results.filter(r => r.success).length; + this.logger.log(`Generación de audios completada: ${successful} exitosos, ${skipped} omitidos`); + + return results; + } + + async getStats(): Promise<{ + total: number; + withDescriptionEs: number; + withDescriptionEn: number; + withAudioEs: number; + withAudioEn: number; + pending: number; + }> { + const all = await this.placeRepository.find({ where: { active: true } }); + + return { + total: all.length, + withDescriptionEs: all.filter(p => p.descriptionEs).length, + withDescriptionEn: all.filter(p => p.descriptionEn).length, + withAudioEs: all.filter(p => p.audioUrlEs).length, + withAudioEn: all.filter(p => p.audioUrlEn).length, + pending: all.filter(p => !p.descriptionEs).length, + }; + } + + private delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} diff --git a/src/modules/content-generator/index.ts b/src/modules/content-generator/index.ts new file mode 100644 index 0000000..6d4e356 --- /dev/null +++ b/src/modules/content-generator/index.ts @@ -0,0 +1,3 @@ +export * from './content-generator.module'; +export * from './content-generator.service'; +export * from './content-generator.controller'; diff --git a/src/modules/favorites/dto/create-favorite.dto.ts b/src/modules/favorites/dto/create-favorite.dto.ts new file mode 100644 index 0000000..f2b3fcf --- /dev/null +++ b/src/modules/favorites/dto/create-favorite.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsUUID, IsString, IsOptional, IsObject } from 'class-validator'; +import { FavoriteItemType } from '../../../entities/user-favorite.entity'; + +export class CreateFavoriteDto { + @ApiProperty({ description: 'Item ID to favorite', example: 'f47ac10b-58cc-4372-a567-0e02b2c3d479' }) + @IsUUID() + itemId: string; + + @ApiProperty({ description: 'Type of item', enum: FavoriteItemType, example: FavoriteItemType.PLACE }) + @IsEnum(FavoriteItemType) + itemType: FavoriteItemType; + + @ApiPropertyOptional({ description: 'Item name for display', example: 'Zona Colonial' }) + @IsOptional() + @IsString() + itemName?: string; + + @ApiPropertyOptional({ description: 'Item image URL', example: 'https://example.com/image.jpg' }) + @IsOptional() + @IsString() + itemImageUrl?: string; + + @ApiPropertyOptional({ description: 'Additional item metadata' }) + @IsOptional() + @IsObject() + itemMetadata?: Record; + + @ApiPropertyOptional({ description: 'User notes', example: 'Must visit this place!' }) + @IsOptional() + @IsString() + notes?: string; +} diff --git a/src/modules/favorites/dto/index.ts b/src/modules/favorites/dto/index.ts new file mode 100644 index 0000000..4a8e812 --- /dev/null +++ b/src/modules/favorites/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-favorite.dto'; +export * from './update-favorite.dto'; diff --git a/src/modules/favorites/dto/update-favorite.dto.ts b/src/modules/favorites/dto/update-favorite.dto.ts new file mode 100644 index 0000000..4532a14 --- /dev/null +++ b/src/modules/favorites/dto/update-favorite.dto.ts @@ -0,0 +1,14 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsString, IsOptional, IsObject } from 'class-validator'; + +export class UpdateFavoriteDto { + @ApiPropertyOptional({ description: 'User notes', example: 'Updated notes' }) + @IsOptional() + @IsString() + notes?: string; + + @ApiPropertyOptional({ description: 'Additional item metadata' }) + @IsOptional() + @IsObject() + itemMetadata?: Record; +} diff --git a/src/modules/favorites/favorites.controller.ts b/src/modules/favorites/favorites.controller.ts new file mode 100644 index 0000000..f599816 --- /dev/null +++ b/src/modules/favorites/favorites.controller.ts @@ -0,0 +1,125 @@ +import { + Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, Request, +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam, +} from '@nestjs/swagger'; +import { FavoritesService } from './favorites.service'; +import { CreateFavoriteDto } from './dto/create-favorite.dto'; +import { UpdateFavoriteDto } from './dto/update-favorite.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { UserFavorite, FavoriteItemType } from '../../entities/user-favorite.entity'; + +@ApiTags('Favorites') +@Controller('favorites') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class FavoritesController { + constructor(private readonly favoritesService: FavoritesService) {} + + @Post() + @ApiOperation({ summary: 'Add item to favorites' }) + @ApiResponse({ status: 201, description: 'Item added to favorites', type: UserFavorite }) + @ApiResponse({ status: 409, description: 'Item already in favorites' }) + create(@Body() createFavoriteDto: CreateFavoriteDto, @Request() req) { + return this.favoritesService.create(req.user.id, createFavoriteDto); + } + + @Get('my') + @ApiOperation({ summary: 'Get current user favorites' }) + @ApiQuery({ name: 'type', required: false, enum: FavoriteItemType, description: 'Filter by item type' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiResponse({ status: 200, description: 'User favorites retrieved' }) + findMyFavorites( + @Request() req, + @Query('type') type?: FavoriteItemType, + @Query('page') page?: number, + @Query('limit') limit?: number, + ) { + return this.favoritesService.findAllByUser(req.user.id, type, page, limit); + } + + @Get('my/counts') + @ApiOperation({ summary: 'Get favorites count by type' }) + @ApiResponse({ status: 200, description: 'Favorites counts by type' }) + getFavoritesCounts(@Request() req) { + return this.favoritesService.getFavoritesCount(req.user.id); + } + + @Get('check/:itemType/:itemId') + @ApiOperation({ summary: 'Check if item is favorited' }) + @ApiParam({ name: 'itemType', enum: FavoriteItemType }) + @ApiParam({ name: 'itemId', type: 'string' }) + @ApiResponse({ status: 200, schema: { type: 'object', properties: { isFavorited: { type: 'boolean' } } } }) + checkFavorite( + @Request() req, + @Param('itemType') itemType: FavoriteItemType, + @Param('itemId') itemId: string, + ) { + return this.favoritesService.isFavorited(req.user.id, itemId, itemType).then(isFavorited => ({ isFavorited })); + } + + @Post('toggle') + @ApiOperation({ summary: 'Toggle favorite status for an item' }) + @ApiResponse({ status: 200, description: 'Favorite toggled' }) + toggleFavorite( + @Request() req, + @Body() body: { + itemId: string; + itemType: FavoriteItemType; + name?: string; + imageUrl?: string; + metadata?: Record; + }, + ) { + return this.favoritesService.toggleFavorite( + req.user.id, + body.itemId, + body.itemType, + { name: body.name, imageUrl: body.imageUrl, metadata: body.metadata } + ); + } + + @Get(':id') + @ApiOperation({ summary: 'Get favorite by ID' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, type: UserFavorite }) + @ApiResponse({ status: 404, description: 'Favorite not found' }) + findOne(@Param('id') id: string, @Request() req) { + return this.favoritesService.findOne(req.user.id, id); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update favorite (notes, metadata)' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, type: UserFavorite }) + update( + @Param('id') id: string, + @Body() updateFavoriteDto: UpdateFavoriteDto, + @Request() req, + ) { + return this.favoritesService.update(req.user.id, id, updateFavoriteDto); + } + + @Delete(':id') + @ApiOperation({ summary: 'Remove from favorites' }) + @ApiParam({ name: 'id', type: 'string' }) + @ApiResponse({ status: 200, description: 'Removed from favorites' }) + remove(@Param('id') id: string, @Request() req) { + return this.favoritesService.remove(req.user.id, id); + } + + @Delete('item/:itemType/:itemId') + @ApiOperation({ summary: 'Remove from favorites by item ID and type' }) + @ApiParam({ name: 'itemType', enum: FavoriteItemType }) + @ApiParam({ name: 'itemId', type: 'string' }) + @ApiResponse({ status: 200, description: 'Removed from favorites' }) + removeByItem( + @Param('itemType') itemType: FavoriteItemType, + @Param('itemId') itemId: string, + @Request() req, + ) { + return this.favoritesService.removeByItem(req.user.id, itemId, itemType); + } +} diff --git a/src/modules/favorites/favorites.module.ts b/src/modules/favorites/favorites.module.ts new file mode 100644 index 0000000..d612897 --- /dev/null +++ b/src/modules/favorites/favorites.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { FavoritesService } from './favorites.service'; +import { FavoritesController } from './favorites.controller'; +import { UserFavorite } from '../../entities/user-favorite.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([UserFavorite])], + controllers: [FavoritesController], + providers: [FavoritesService], + exports: [FavoritesService], +}) +export class FavoritesModule {} diff --git a/src/modules/favorites/favorites.service.ts b/src/modules/favorites/favorites.service.ts new file mode 100644 index 0000000..746ab41 --- /dev/null +++ b/src/modules/favorites/favorites.service.ts @@ -0,0 +1,149 @@ +import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { UserFavorite, FavoriteItemType } from '../../entities/user-favorite.entity'; +import { CreateFavoriteDto } from './dto/create-favorite.dto'; +import { UpdateFavoriteDto } from './dto/update-favorite.dto'; + +@Injectable() +export class FavoritesService { + constructor( + @InjectRepository(UserFavorite) + private readonly favoriteRepository: Repository, + ) {} + + async create(userId: string, createFavoriteDto: CreateFavoriteDto): Promise { + // Check if already favorited + const existing = await this.favoriteRepository.findOne({ + where: { + userId, + itemId: createFavoriteDto.itemId, + itemType: createFavoriteDto.itemType, + }, + }); + + if (existing) { + throw new ConflictException('Item already in favorites'); + } + + const favorite = this.favoriteRepository.create({ + userId, + ...createFavoriteDto, + }); + + return this.favoriteRepository.save(favorite); + } + + async findAllByUser( + userId: string, + itemType?: FavoriteItemType, + page = 1, + limit = 20, + ): Promise<{ data: UserFavorite[]; total: number; page: number; limit: number }> { + const where: any = { userId }; + + if (itemType) { + where.itemType = itemType; + } + + const [data, total] = await this.favoriteRepository.findAndCount({ + where, + order: { createdAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, + }); + + return { data, total, page, limit }; + } + + async findOne(userId: string, id: string): Promise { + const favorite = await this.favoriteRepository.findOne({ + where: { id, userId }, + }); + + if (!favorite) { + throw new NotFoundException('Favorite not found'); + } + + return favorite; + } + + async update(userId: string, id: string, updateFavoriteDto: UpdateFavoriteDto): Promise { + const favorite = await this.findOne(userId, id); + + Object.assign(favorite, updateFavoriteDto); + + return this.favoriteRepository.save(favorite); + } + + async remove(userId: string, id: string): Promise { + const favorite = await this.findOne(userId, id); + await this.favoriteRepository.remove(favorite); + } + + async removeByItem(userId: string, itemId: string, itemType: FavoriteItemType): Promise { + const favorite = await this.favoriteRepository.findOne({ + where: { userId, itemId, itemType }, + }); + + if (favorite) { + await this.favoriteRepository.remove(favorite); + } + } + + async isFavorited(userId: string, itemId: string, itemType: FavoriteItemType): Promise { + const count = await this.favoriteRepository.count({ + where: { userId, itemId, itemType }, + }); + return count > 0; + } + + async getFavoritesCount(userId: string): Promise> { + const counts = await this.favoriteRepository + .createQueryBuilder('favorite') + .select('favorite.item_type', 'itemType') + .addSelect('COUNT(*)', 'count') + .where('favorite.user_id = :userId', { userId }) + .groupBy('favorite.item_type') + .getRawMany(); + + const result: Record = {}; + Object.values(FavoriteItemType).forEach(type => { + result[type] = 0; + }); + + counts.forEach(c => { + result[c.itemType] = parseInt(c.count, 10); + }); + + return result as Record; + } + + async toggleFavorite( + userId: string, + itemId: string, + itemType: FavoriteItemType, + itemData?: { name?: string; imageUrl?: string; metadata?: Record } + ): Promise<{ isFavorited: boolean; favorite?: UserFavorite }> { + const existing = await this.favoriteRepository.findOne({ + where: { userId, itemId, itemType }, + }); + + if (existing) { + await this.favoriteRepository.remove(existing); + return { isFavorited: false }; + } + + const favorite = this.favoriteRepository.create({ + userId, + itemId, + itemType, + itemName: itemData?.name, + itemImageUrl: itemData?.imageUrl, + itemMetadata: itemData?.metadata, + }); + + const saved = await this.favoriteRepository.save(favorite); + return { isFavorited: true, favorite: saved }; + } +} diff --git a/src/modules/geolocation/geolocation.controller.ts b/src/modules/geolocation/geolocation.controller.ts index 9b953d7..992fa94 100644 --- a/src/modules/geolocation/geolocation.controller.ts +++ b/src/modules/geolocation/geolocation.controller.ts @@ -88,16 +88,23 @@ export class GeolocationController { } @Get('nearby/attractions') - @ApiOperation({ summary: 'Get nearby attractions' }) + @ApiOperation({ summary: 'Get nearby attractions with multi-language support' }) @ApiQuery({ name: 'latitude', type: Number }) @ApiQuery({ name: 'longitude', type: Number }) - @ApiQuery({ name: 'radius', required: false, type: Number, description: 'Radius in meters' }) + @ApiQuery({ name: 'radius', required: false, type: Number, description: 'Radius in meters (default: 1000)' }) + @ApiQuery({ name: 'language', required: false, type: String, description: 'Language code (es, en, fr, it, de)' }) async getNearbyAttractions( @Query('latitude') latitude: number, @Query('longitude') longitude: number, @Query('radius') radius?: number, + @Query('language') language?: string, ) { - return this.geolocationService['getNearbyAttractions'](latitude, longitude, radius); + return this.geolocationService.getNearbyAttractions( + latitude, + longitude, + radius || 1000, + language || 'es', + ); } // SMART SUGGESTIONS (REQUIERE USUARIO) diff --git a/src/modules/geolocation/geolocation.service.ts b/src/modules/geolocation/geolocation.service.ts index 736d5c2..77e2572 100644 --- a/src/modules/geolocation/geolocation.service.ts +++ b/src/modules/geolocation/geolocation.service.ts @@ -180,16 +180,72 @@ export class GeolocationService { }; } - private async getNearbyAttractions( + // NEARBY ATTRACTIONS WITH DISTANCE FILTERING AND MULTI-LANGUAGE SUPPORT + async getNearbyAttractions( latitude: number, longitude: number, radiusMeters: number = 1000, - ): Promise { - // In production, use PostGIS ST_DWithin for accurate distance queries - return this.placeRepository.find({ + language: string = 'es', + ): Promise { + // Get all active places + const allPlaces = await this.placeRepository.find({ where: { active: true }, - order: { rating: 'DESC' }, - take: 10, + }); + + // Filter by actual distance and add distance info + const nearbyPlaces = allPlaces + .map(place => { + const placeLat = this.extractLatFromPoint(place.coordinates); + const placeLng = this.extractLngFromPoint(place.coordinates); + const distance = this.calculateDistance(latitude, longitude, placeLat, placeLng); + + return { + ...place, + distance, + distanceFormatted: distance < 1000 + ? `${Math.round(distance)}m` + : `${(distance / 1000).toFixed(1)}km`, + }; + }) + .filter(place => place.distance <= radiusMeters) + .sort((a, b) => a.distance - b.distance); + + // Language mapping for camelCase property names (matching TypeORM entity) + const langMap: Record = { + es: { descKey: 'descriptionEs', audioKey: 'audioUrlEs' }, + en: { descKey: 'descriptionEn', audioKey: 'audioUrlEn' }, + fr: { descKey: 'descriptionFr', audioKey: 'audioUrlFr' }, + it: { descKey: 'descriptionIt', audioKey: 'audioUrlIt' }, + de: { descKey: 'descriptionDe', audioKey: 'audioUrlDe' }, + }; + + const lang = langMap[language] || langMap['es']; + + // Map to response with language-specific content + return nearbyPlaces.slice(0, 10).map(place => { + const description = (place as any)[lang.descKey] || place.descriptionEs || null; + const audioUrl = (place as any)[lang.audioKey] || null; + + return { + id: place.id, + name: place.name, + slug: place.slug, + category: place.category, + country: place.country, + address: place.address, + coordinates: { + latitude: this.extractLatFromPoint(place.coordinates), + longitude: this.extractLngFromPoint(place.coordinates), + }, + distance: place.distance, + distanceFormatted: place.distanceFormatted, + description, + audioUrl, + voiceId: place.voiceId || 'es_DO-keira-medium', + rating: place.rating, + featured: place.featured || false, + images: place.images, + }; }); } diff --git a/src/modules/geolocation/geolocation.service.ts.backup b/src/modules/geolocation/geolocation.service.ts.backup new file mode 100644 index 0000000..f72fc17 --- /dev/null +++ b/src/modules/geolocation/geolocation.service.ts.backup @@ -0,0 +1,445 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ConfigService } from '@nestjs/config'; +import axios from 'axios'; +import { Geofence } from '../../entities/geofence.entity'; +import { LocationTracking } from '../../entities/location-tracking.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { CreateGeofenceDto } from './dto/geofence.dto'; +import { LocationUpdateDto } from './dto/location-update.dto'; +import { NotificationsService } from '../notifications/notifications.service'; +import { SecurityService } from '../security/security.service'; +import { NotificationType, NotificationCategory } from '../notifications/dto/create-notification.dto'; + +@Injectable() +export class GeolocationService { + constructor( + @InjectRepository(Geofence) + private readonly geofenceRepository: Repository, + @InjectRepository(LocationTracking) + private readonly locationTrackingRepository: Repository, + @InjectRepository(PlaceOfInterest) + private readonly placeRepository: Repository, + private readonly configService: ConfigService, + private readonly notificationsService: NotificationsService, + private readonly securityService: SecurityService, + ) { } + + // GEOFENCING MANAGEMENT + async createGeofence(createGeofenceDto: CreateGeofenceDto): Promise { + const geofence = this.geofenceRepository.create({ + ...createGeofenceDto, + centerCoordinates: `(${createGeofenceDto.longitude},${createGeofenceDto.latitude})`, + }); + + return this.geofenceRepository.save(geofence); + } + + async getActiveGeofences(): Promise { + return this.geofenceRepository.find({ + where: { isActive: true }, + order: { createdAt: 'DESC' }, + }); + } + + async checkGeofenceEntry( + userId: string, + latitude: number, + longitude: number, + ): Promise<{ + triggeredGeofences: Geofence[]; + alerts: Array<{ type: string; message: string; geofence: Geofence }>; + }> { + // In production, use PostGIS for accurate geospatial calculations + // For now, simulate geofence detection + const activeGeofences = await this.getActiveGeofences(); + const triggeredGeofences: Geofence[] = []; + const alerts: Array<{ type: string; message: string; geofence: Geofence }> = []; + + for (const geofence of activeGeofences) { + const distance = this.calculateDistance( + latitude, + longitude, + this.extractLatFromPoint(geofence.centerCoordinates), + this.extractLngFromPoint(geofence.centerCoordinates) + ); + + if (distance <= geofence.radius) { + triggeredGeofences.push(geofence); + + // Increment entry count + await this.geofenceRepository.increment({ id: geofence.id }, 'entryCount', 1); + + // Handle different geofence types + switch (geofence.type) { + case 'safety-alert': + alerts.push({ + type: 'warning', + message: geofence.entryMessage || 'You have entered a safety alert zone. Please be cautious.', + geofence, + }); + break; + + case 'tourist-zone': + alerts.push({ + type: 'info', + message: geofence.entryMessage || `Welcome to ${geofence.name}! Explore the attractions around you.`, + geofence, + }); + break; + + case 'attraction': + alerts.push({ + type: 'attraction', + message: geofence.entryMessage || `You're near ${geofence.name}. Check out what's available!`, + geofence, + }); + break; + + case 'restricted-area': + alerts.push({ + type: 'restriction', + message: geofence.entryMessage || 'You have entered a restricted area. Please respect local regulations.', + geofence, + }); + break; + + case 'pickup-zone': + alerts.push({ + type: 'service', + message: geofence.entryMessage || 'You are in a designated pickup zone for taxis and tours.', + geofence, + }); + break; + } + + // Send notification + await this.notificationsService.createNotification({ + userId, + type: NotificationType.PUSH, + category: NotificationCategory.SECURITY, + title: `📍 ${geofence.name}`, + message: alerts[alerts.length - 1]?.message || 'Location alert', + data: { geofenceId: geofence.id, type: geofence.type }, + }); + } + } + + return { triggeredGeofences, alerts }; + } + + // LOCATION TRACKING + async updateUserLocation( + userId: string, + locationDto: LocationUpdateDto, + ): Promise<{ + success: boolean; + geofenceAlerts: any[]; + nearbyAttractions: PlaceOfInterest[]; + smartSuggestions: string[]; + }> { + // Save location tracking + const tracking = this.locationTrackingRepository.create({ + userId, + coordinates: `(${locationDto.longitude},${locationDto.latitude})`, + accuracy: locationDto.accuracy, + speed: locationDto.speed, + heading: locationDto.heading, + activity: locationDto.activity, + }); + + await this.locationTrackingRepository.save(tracking); + + // Check geofences + const geofenceCheck = await this.checkGeofenceEntry( + userId, + locationDto.latitude, + locationDto.longitude, + ); + + // Get nearby attractions + const nearbyAttractions = await this.getNearbyAttractions( + locationDto.latitude, + locationDto.longitude, + 1000, // 1km radius + ); + + // Generate smart suggestions based on location and activity + const smartSuggestions = await this.generateSmartSuggestions( + userId, + locationDto, + nearbyAttractions, + ); + + return { + success: true, + geofenceAlerts: geofenceCheck.alerts, + nearbyAttractions, + smartSuggestions, + }; + } + + // NEARBY ATTRACTIONS WITH DISTANCE FILTERING AND MULTI-LANGUAGE SUPPORT + async getNearbyAttractions( + latitude: number, + longitude: number, + radiusMeters: number = 1000, + language: string = 'es', + ): Promise { + // Get all active places + const allPlaces = await this.placeRepository.find({ + where: { active: true }, + }); + + // Filter by actual distance and add distance info + const nearbyPlaces = allPlaces + .map(place => { + const placeLat = this.extractLatFromPoint(place.coordinates); + const placeLng = this.extractLngFromPoint(place.coordinates); + const distance = this.calculateDistance(latitude, longitude, placeLat, placeLng); + + return { + ...place, + distance, + distanceFormatted: distance < 1000 + ? `${Math.round(distance)}m` + : `${(distance / 1000).toFixed(1)}km`, + }; + }) + .filter(place => place.distance <= radiusMeters) + .sort((a, b) => a.distance - b.distance); + + // Map to response with language-specific content + return nearbyPlaces.slice(0, 10).map(place => { + const descriptionKey = `description_${language}` as keyof typeof place; + const audioKey = `audio_url_${language}` as keyof typeof place; + + return { + id: place.id, + name: place.name, + slug: (place as any).slug, + category: place.category, + country: (place as any).country, + address: place.address, + coordinates: { + latitude: this.extractLatFromPoint(place.coordinates), + longitude: this.extractLngFromPoint(place.coordinates), + }, + distance: place.distance, + distanceFormatted: place.distanceFormatted, + description: (place as any)[descriptionKey] || (place as any).description_es || null, + audioUrl: (place as any)[audioKey] || null, + voiceId: (place as any).voice_id || 'es_DO-keira-medium', + rating: place.rating, + featured: (place as any).featured || false, + images: place.images, + }; + }); + } + + private async generateSmartSuggestions( + userId: string, + location: LocationUpdateDto, + nearbyAttractions: PlaceOfInterest[], + ): Promise { + const suggestions: string[] = []; + const currentHour = new Date().getHours(); + + // Time-based suggestions + if (currentHour >= 6 && currentHour <= 10) { + suggestions.push("Good morning! Start your day with a visit to a nearby historic site."); + } else if (currentHour >= 11 && currentHour <= 14) { + suggestions.push("It's lunch time! Find authentic Dominican restaurants nearby."); + } else if (currentHour >= 15 && currentHour <= 18) { + suggestions.push("Perfect time for sightseeing! Explore attractions around you."); + } else if (currentHour >= 19 && currentHour <= 23) { + suggestions.push("Evening entertainment awaits! Check out local restaurants and nightlife."); + } + + // Activity-based suggestions + if (location.activity === 'walking') { + suggestions.push("Great walking weather! Discover hidden gems on foot."); + } else if (location.activity === 'stationary') { + suggestions.push("Take a moment to explore what's around you."); + } + + // Speed-based suggestions + if (location.speed && location.speed > 30) { + suggestions.push("Traveling fast? Don't miss scenic viewpoints along your route."); + } + + // Attraction-based suggestions + if (nearbyAttractions.length > 0) { + const topAttraction = nearbyAttractions[0]; + suggestions.push(`${topAttraction.name} is nearby (${topAttraction.rating}/5 stars). Worth a visit!`); + } + + return suggestions.slice(0, 3); // Return top 3 suggestions + } + + // SMART NAVIGATION + async getOptimizedRoute( + startLat: number, + startLng: number, + endLat: number, + endLng: number, + travelMode: string = 'walking', + includeAttractions: boolean = true, + ): Promise<{ + route: any; + duration: number; + distance: number; + waypoints: PlaceOfInterest[]; + weatherInfo: any; + safetyTips: string[]; + }> { + // In production, integrate with Google Maps Directions API + const mockRoute = { + steps: [ + { instruction: 'Head north on Calle Las Damas', distance: '200m', duration: '3 min' }, + { instruction: 'Turn right at Plaza de Armas', distance: '150m', duration: '2 min' }, + { instruction: 'Continue straight to destination', distance: '100m', duration: '1 min' }, + ], + }; + + const waypoints = includeAttractions + ? await this.getWaypointAttractions(startLat, startLng, endLat, endLng) + : []; + + const weatherInfo = await this.getRouteWeatherInfo(startLat, startLng); + const safetyTips = this.generateRouteSafetyTips(travelMode, new Date().getHours()); + + return { + route: mockRoute, + duration: 6, // minutes + distance: 450, // meters + waypoints, + weatherInfo, + safetyTips, + }; + } + + private async getWaypointAttractions( + startLat: number, + startLng: number, + endLat: number, + endLng: number, + ): Promise { + // Find attractions along the route + return this.placeRepository.find({ + where: { active: true }, + order: { rating: 'DESC' }, + take: 3, + }); + } + + private async getRouteWeatherInfo(lat: number, lng: number): Promise { + // In production, integrate with weather API + return { + temperature: 28, + condition: 'sunny', + humidity: 75, + recommendation: 'Perfect weather for walking! Stay hydrated.', + }; + } + + private generateRouteSafetyTips(travelMode: string, hour: number): string[] { + const tips: string[] = [ + 'Stay aware of your surroundings', + 'Keep your belongings secure', + ]; + + if (travelMode === 'walking') { + tips.push('Use sidewalks and well-lit paths'); + if (hour >= 20 || hour <= 6) { + tips.push('Consider using a taxi for night travel'); + } + } + + if (travelMode === 'driving') { + tips.push('Follow local traffic laws'); + tips.push('Use GPS navigation'); + } + + return tips; + } + + // ANALYTICS + async getLocationAnalytics(timeframe: string = '7d'): Promise<{ + totalUsers: number; + activeUsers: number; + popularZones: Array<{ name: string; visits: number }>; + geofenceStats: Array<{ name: string; entries: number; type: string }>; + heatmapData: Array<{ lat: number; lng: number; intensity: number }>; + }> { + const days = timeframe === '7d' ? 7 : timeframe === '30d' ? 30 : 90; + const totalUsers = await this.locationTrackingRepository + .createQueryBuilder('tracking') + .select('COUNT(DISTINCT tracking.userId)', 'count') + .where(`tracking.createdAt >= NOW() - INTERVAL '${days} days'`) + .getRawOne(); + + const activeUsers = await this.locationTrackingRepository + .createQueryBuilder('tracking') + .select('COUNT(DISTINCT tracking.userId)', 'count') + .where(`tracking.createdAt >= NOW() - INTERVAL '1 days'`) + .getRawOne(); + + const geofenceStats = await this.geofenceRepository.find({ + where: { isActive: true }, + order: { entryCount: 'DESC' }, + take: 10, + }); + + // Simplified heatmap data (in production, aggregate actual location data) + const heatmapData = [ + { lat: 18.4861, lng: -69.9312, intensity: 0.8 }, // Santo Domingo + { lat: 18.5204, lng: -68.7340, intensity: 0.6 }, // Punta Cana + { lat: 19.7933, lng: -70.6928, intensity: 0.5 }, // Puerto Plata + ]; + + return { + totalUsers: parseInt(totalUsers.count), + activeUsers: parseInt(activeUsers.count), + popularZones: [], // TODO: Implement zone analysis + geofenceStats: geofenceStats.map(g => ({ + name: g.name, + entries: g.entryCount, + type: g.type, + })), + heatmapData, + }; + } + + // UTILITY METHODS + private calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number { + const R = 6371000; // Earth's radius in meters + const dLat = this.toRadians(lat2 - lat1); + const dLng = this.toRadians(lng2 - lng1); + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; + } + + private toRadians(degrees: number): number { + return degrees * (Math.PI / 180); + } + + private extractLatFromPoint(point: any): number { + if (point && typeof point === 'object' && point.y !== undefined) { + return point.y; // latitude + } + return 0; + } + + + private extractLngFromPoint(point: any): number { + if (point && typeof point === 'object' && point.x !== undefined) { + return point.x; // longitude + } + return 0; + } +} diff --git a/src/modules/geolocation/geolocation.service.ts.bak b/src/modules/geolocation/geolocation.service.ts.bak new file mode 100644 index 0000000..736d5c2 --- /dev/null +++ b/src/modules/geolocation/geolocation.service.ts.bak @@ -0,0 +1,400 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ConfigService } from '@nestjs/config'; +import axios from 'axios'; +import { Geofence } from '../../entities/geofence.entity'; +import { LocationTracking } from '../../entities/location-tracking.entity'; +import { PlaceOfInterest } from '../../entities/place-of-interest.entity'; +import { CreateGeofenceDto } from './dto/geofence.dto'; +import { LocationUpdateDto } from './dto/location-update.dto'; +import { NotificationsService } from '../notifications/notifications.service'; +import { SecurityService } from '../security/security.service'; +import { NotificationType, NotificationCategory } from '../notifications/dto/create-notification.dto'; + +@Injectable() +export class GeolocationService { + constructor( + @InjectRepository(Geofence) + private readonly geofenceRepository: Repository, + @InjectRepository(LocationTracking) + private readonly locationTrackingRepository: Repository, + @InjectRepository(PlaceOfInterest) + private readonly placeRepository: Repository, + private readonly configService: ConfigService, + private readonly notificationsService: NotificationsService, + private readonly securityService: SecurityService, + ) { } + + // GEOFENCING MANAGEMENT + async createGeofence(createGeofenceDto: CreateGeofenceDto): Promise { + const geofence = this.geofenceRepository.create({ + ...createGeofenceDto, + centerCoordinates: `(${createGeofenceDto.longitude},${createGeofenceDto.latitude})`, + }); + + return this.geofenceRepository.save(geofence); + } + + async getActiveGeofences(): Promise { + return this.geofenceRepository.find({ + where: { isActive: true }, + order: { createdAt: 'DESC' }, + }); + } + + async checkGeofenceEntry( + userId: string, + latitude: number, + longitude: number, + ): Promise<{ + triggeredGeofences: Geofence[]; + alerts: Array<{ type: string; message: string; geofence: Geofence }>; + }> { + // In production, use PostGIS for accurate geospatial calculations + // For now, simulate geofence detection + const activeGeofences = await this.getActiveGeofences(); + const triggeredGeofences: Geofence[] = []; + const alerts: Array<{ type: string; message: string; geofence: Geofence }> = []; + + for (const geofence of activeGeofences) { + const distance = this.calculateDistance( + latitude, + longitude, + this.extractLatFromPoint(geofence.centerCoordinates), + this.extractLngFromPoint(geofence.centerCoordinates) + ); + + if (distance <= geofence.radius) { + triggeredGeofences.push(geofence); + + // Increment entry count + await this.geofenceRepository.increment({ id: geofence.id }, 'entryCount', 1); + + // Handle different geofence types + switch (geofence.type) { + case 'safety-alert': + alerts.push({ + type: 'warning', + message: geofence.entryMessage || 'You have entered a safety alert zone. Please be cautious.', + geofence, + }); + break; + + case 'tourist-zone': + alerts.push({ + type: 'info', + message: geofence.entryMessage || `Welcome to ${geofence.name}! Explore the attractions around you.`, + geofence, + }); + break; + + case 'attraction': + alerts.push({ + type: 'attraction', + message: geofence.entryMessage || `You're near ${geofence.name}. Check out what's available!`, + geofence, + }); + break; + + case 'restricted-area': + alerts.push({ + type: 'restriction', + message: geofence.entryMessage || 'You have entered a restricted area. Please respect local regulations.', + geofence, + }); + break; + + case 'pickup-zone': + alerts.push({ + type: 'service', + message: geofence.entryMessage || 'You are in a designated pickup zone for taxis and tours.', + geofence, + }); + break; + } + + // Send notification + await this.notificationsService.createNotification({ + userId, + type: NotificationType.PUSH, + category: NotificationCategory.SECURITY, + title: `📍 ${geofence.name}`, + message: alerts[alerts.length - 1]?.message || 'Location alert', + data: { geofenceId: geofence.id, type: geofence.type }, + }); + } + } + + return { triggeredGeofences, alerts }; + } + + // LOCATION TRACKING + async updateUserLocation( + userId: string, + locationDto: LocationUpdateDto, + ): Promise<{ + success: boolean; + geofenceAlerts: any[]; + nearbyAttractions: PlaceOfInterest[]; + smartSuggestions: string[]; + }> { + // Save location tracking + const tracking = this.locationTrackingRepository.create({ + userId, + coordinates: `(${locationDto.longitude},${locationDto.latitude})`, + accuracy: locationDto.accuracy, + speed: locationDto.speed, + heading: locationDto.heading, + activity: locationDto.activity, + }); + + await this.locationTrackingRepository.save(tracking); + + // Check geofences + const geofenceCheck = await this.checkGeofenceEntry( + userId, + locationDto.latitude, + locationDto.longitude, + ); + + // Get nearby attractions + const nearbyAttractions = await this.getNearbyAttractions( + locationDto.latitude, + locationDto.longitude, + 1000, // 1km radius + ); + + // Generate smart suggestions based on location and activity + const smartSuggestions = await this.generateSmartSuggestions( + userId, + locationDto, + nearbyAttractions, + ); + + return { + success: true, + geofenceAlerts: geofenceCheck.alerts, + nearbyAttractions, + smartSuggestions, + }; + } + + private async getNearbyAttractions( + latitude: number, + longitude: number, + radiusMeters: number = 1000, + ): Promise { + // In production, use PostGIS ST_DWithin for accurate distance queries + return this.placeRepository.find({ + where: { active: true }, + order: { rating: 'DESC' }, + take: 10, + }); + } + + private async generateSmartSuggestions( + userId: string, + location: LocationUpdateDto, + nearbyAttractions: PlaceOfInterest[], + ): Promise { + const suggestions: string[] = []; + const currentHour = new Date().getHours(); + + // Time-based suggestions + if (currentHour >= 6 && currentHour <= 10) { + suggestions.push("Good morning! Start your day with a visit to a nearby historic site."); + } else if (currentHour >= 11 && currentHour <= 14) { + suggestions.push("It's lunch time! Find authentic Dominican restaurants nearby."); + } else if (currentHour >= 15 && currentHour <= 18) { + suggestions.push("Perfect time for sightseeing! Explore attractions around you."); + } else if (currentHour >= 19 && currentHour <= 23) { + suggestions.push("Evening entertainment awaits! Check out local restaurants and nightlife."); + } + + // Activity-based suggestions + if (location.activity === 'walking') { + suggestions.push("Great walking weather! Discover hidden gems on foot."); + } else if (location.activity === 'stationary') { + suggestions.push("Take a moment to explore what's around you."); + } + + // Speed-based suggestions + if (location.speed && location.speed > 30) { + suggestions.push("Traveling fast? Don't miss scenic viewpoints along your route."); + } + + // Attraction-based suggestions + if (nearbyAttractions.length > 0) { + const topAttraction = nearbyAttractions[0]; + suggestions.push(`${topAttraction.name} is nearby (${topAttraction.rating}/5 stars). Worth a visit!`); + } + + return suggestions.slice(0, 3); // Return top 3 suggestions + } + + // SMART NAVIGATION + async getOptimizedRoute( + startLat: number, + startLng: number, + endLat: number, + endLng: number, + travelMode: string = 'walking', + includeAttractions: boolean = true, + ): Promise<{ + route: any; + duration: number; + distance: number; + waypoints: PlaceOfInterest[]; + weatherInfo: any; + safetyTips: string[]; + }> { + // In production, integrate with Google Maps Directions API + const mockRoute = { + steps: [ + { instruction: 'Head north on Calle Las Damas', distance: '200m', duration: '3 min' }, + { instruction: 'Turn right at Plaza de Armas', distance: '150m', duration: '2 min' }, + { instruction: 'Continue straight to destination', distance: '100m', duration: '1 min' }, + ], + }; + + const waypoints = includeAttractions + ? await this.getWaypointAttractions(startLat, startLng, endLat, endLng) + : []; + + const weatherInfo = await this.getRouteWeatherInfo(startLat, startLng); + const safetyTips = this.generateRouteSafetyTips(travelMode, new Date().getHours()); + + return { + route: mockRoute, + duration: 6, // minutes + distance: 450, // meters + waypoints, + weatherInfo, + safetyTips, + }; + } + + private async getWaypointAttractions( + startLat: number, + startLng: number, + endLat: number, + endLng: number, + ): Promise { + // Find attractions along the route + return this.placeRepository.find({ + where: { active: true }, + order: { rating: 'DESC' }, + take: 3, + }); + } + + private async getRouteWeatherInfo(lat: number, lng: number): Promise { + // In production, integrate with weather API + return { + temperature: 28, + condition: 'sunny', + humidity: 75, + recommendation: 'Perfect weather for walking! Stay hydrated.', + }; + } + + private generateRouteSafetyTips(travelMode: string, hour: number): string[] { + const tips: string[] = [ + 'Stay aware of your surroundings', + 'Keep your belongings secure', + ]; + + if (travelMode === 'walking') { + tips.push('Use sidewalks and well-lit paths'); + if (hour >= 20 || hour <= 6) { + tips.push('Consider using a taxi for night travel'); + } + } + + if (travelMode === 'driving') { + tips.push('Follow local traffic laws'); + tips.push('Use GPS navigation'); + } + + return tips; + } + + // ANALYTICS + async getLocationAnalytics(timeframe: string = '7d'): Promise<{ + totalUsers: number; + activeUsers: number; + popularZones: Array<{ name: string; visits: number }>; + geofenceStats: Array<{ name: string; entries: number; type: string }>; + heatmapData: Array<{ lat: number; lng: number; intensity: number }>; + }> { + const days = timeframe === '7d' ? 7 : timeframe === '30d' ? 30 : 90; + const totalUsers = await this.locationTrackingRepository + .createQueryBuilder('tracking') + .select('COUNT(DISTINCT tracking.userId)', 'count') + .where(`tracking.createdAt >= NOW() - INTERVAL '${days} days'`) + .getRawOne(); + + const activeUsers = await this.locationTrackingRepository + .createQueryBuilder('tracking') + .select('COUNT(DISTINCT tracking.userId)', 'count') + .where(`tracking.createdAt >= NOW() - INTERVAL '1 days'`) + .getRawOne(); + + const geofenceStats = await this.geofenceRepository.find({ + where: { isActive: true }, + order: { entryCount: 'DESC' }, + take: 10, + }); + + // Simplified heatmap data (in production, aggregate actual location data) + const heatmapData = [ + { lat: 18.4861, lng: -69.9312, intensity: 0.8 }, // Santo Domingo + { lat: 18.5204, lng: -68.7340, intensity: 0.6 }, // Punta Cana + { lat: 19.7933, lng: -70.6928, intensity: 0.5 }, // Puerto Plata + ]; + + return { + totalUsers: parseInt(totalUsers.count), + activeUsers: parseInt(activeUsers.count), + popularZones: [], // TODO: Implement zone analysis + geofenceStats: geofenceStats.map(g => ({ + name: g.name, + entries: g.entryCount, + type: g.type, + })), + heatmapData, + }; + } + + // UTILITY METHODS + private calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number { + const R = 6371000; // Earth's radius in meters + const dLat = this.toRadians(lat2 - lat1); + const dLng = this.toRadians(lng2 - lng1); + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; + } + + private toRadians(degrees: number): number { + return degrees * (Math.PI / 180); + } + + private extractLatFromPoint(point: any): number { + if (point && typeof point === 'object' && point.y !== undefined) { + return point.y; // latitude + } + return 0; + } + + + private extractLngFromPoint(point: any): number { + if (point && typeof point === 'object' && point.x !== undefined) { + return point.x; // longitude + } + return 0; + } +} diff --git a/src/modules/geolocation/geolocation.service.ts.patch b/src/modules/geolocation/geolocation.service.ts.patch new file mode 100644 index 0000000..5a549de --- /dev/null +++ b/src/modules/geolocation/geolocation.service.ts.patch @@ -0,0 +1,68 @@ + private async getNearbyAttractions( + latitude: number, + longitude: number, + radiusMeters: number = 500, + language: string = 'es', + ): Promise { + // Calcular bounding box para filtrar aproximadamente + const latDelta = radiusMeters / 111320; // 1 grado ~ 111km + const lngDelta = radiusMeters / (111320 * Math.cos(latitude * Math.PI / 180)); + + const places = await this.placeRepository + .createQueryBuilder('place') + .where('place.active = :active', { active: true }) + .andWhere('place.coordinates IS NOT NULL') + .orderBy('place.featured', 'DESC') + .addOrderBy('place.rating', 'DESC') + .take(20) + .getMany(); + + // Filtrar y calcular distancia real + const nearbyPlaces = places + .map(place => { + const coords = this.extractCoordsFromPoint(place.coordinates as any); + if (!coords) return null; + + const distance = this.calculateDistance(latitude, longitude, coords.lat, coords.lng); + if (distance > radiusMeters) return null; + + // Seleccionar descripción según idioma + const descriptionKey = `description_${language}` as keyof typeof place; + const description = (place as any)[descriptionKey] || place.description || ''; + + return { + id: place.id, + name: place.name, + slug: (place as any).slug, + category: place.category, + distance: Math.round(distance), + coordinates: coords, + thumbnail: place.images?.[0] || null, + shortDescription: description.substring(0, 200), + fullDescription: description, + address: place.address, + rating: place.rating, + country: (place as any).country, + featured: (place as any).featured, + }; + }) + .filter(p => p !== null) + .sort((a, b) => a!.distance - b!.distance); + + return nearbyPlaces; + } + + private extractCoordsFromPoint(point: any): { lat: number; lng: number } | null { + if (!point) return null; + // PostgreSQL point format: (x,y) donde x=lng, y=lat + if (typeof point === 'string') { + const match = point.match(/\(([^,]+),([^)]+)\)/); + if (match) { + return { lng: parseFloat(match[1]), lat: parseFloat(match[2]) }; + } + } + if (point.x !== undefined && point.y !== undefined) { + return { lng: point.x, lat: point.y }; + } + return null; + } diff --git a/src/modules/kimi/index.ts b/src/modules/kimi/index.ts new file mode 100644 index 0000000..006c8bf --- /dev/null +++ b/src/modules/kimi/index.ts @@ -0,0 +1,2 @@ +export * from "./kimi.module"; +export * from "./kimi.service"; diff --git a/src/modules/kimi/kimi.module.ts b/src/modules/kimi/kimi.module.ts new file mode 100644 index 0000000..7519615 --- /dev/null +++ b/src/modules/kimi/kimi.module.ts @@ -0,0 +1,12 @@ +import { Module, Global } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { KimiService } from "./kimi.service"; +import kimiConfig from "../../config/integrations/kimi.config"; + +@Global() +@Module({ + imports: [ConfigModule.forFeature(kimiConfig)], + providers: [KimiService], + exports: [KimiService], +}) +export class KimiModule {} diff --git a/src/modules/kimi/kimi.service.ts b/src/modules/kimi/kimi.service.ts new file mode 100644 index 0000000..960a442 --- /dev/null +++ b/src/modules/kimi/kimi.service.ts @@ -0,0 +1,469 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import OpenAI from 'openai'; + +export interface KimiMessage { + role: 'system' | 'user' | 'assistant'; + content: string; +} + +export interface KimiChatOptions { + temperature?: number; + maxTokens?: number; + topP?: number; + stream?: boolean; +} + +@Injectable() +export class KimiService implements OnModuleInit { + private readonly logger = new Logger(KimiService.name); + private client: OpenAI | null = null; + private model: string; + private defaultTemperature: number; + private defaultMaxTokens: number; + private defaultTopP: number; + private isConfigured: boolean = false; + + constructor(private readonly configService: ConfigService) { + this.model = this.configService.get('kimi.model') || 'kimi-k2.5'; + this.defaultTemperature = this.configService.get('kimi.temperature') || 0.6; + this.defaultMaxTokens = this.configService.get('kimi.maxTokens') || 4096; + this.defaultTopP = this.configService.get('kimi.topP') || 0.95; + } + + async onModuleInit() { + await this.initialize(); + } + + private async initialize(): Promise { + const apiKey = this.configService.get('kimi.apiKey'); + const baseUrl = this.configService.get('kimi.baseUrl'); + + if (!apiKey) { + this.logger.warn('⚠️ KIMI_API_KEY no está configurada. El servicio de IA usará respuestas de fallback.'); + this.isConfigured = false; + return; + } + + try { + this.client = new OpenAI({ + apiKey, + baseURL: baseUrl || 'https://api.moonshot.ai/v1', + }); + + // Test de conexión + this.logger.log('🔧 Inicializando Kimi Service...'); + this.logger.log(`📌 Base URL: ${baseUrl}`); + this.logger.log(`📌 Model: ${this.model}`); + + // Verificar conexión con un mensaje simple + const testResponse = await this.client.chat.completions.create({ + model: this.model, + messages: [{ role: 'user', content: 'Hello' }], + max_tokens: 10, + }); + + if (testResponse.choices?.[0]?.message?.content) { + this.logger.log('✅ Kimi Service inicializado correctamente'); + this.isConfigured = true; + } + } catch (error: any) { + this.logger.error(`❌ Error inicializando Kimi: ${error.message}`); + this.isConfigured = false; + } + } + + /** + * Verifica si el servicio está configurado y funcionando + */ + isReady(): boolean { + return this.isConfigured && this.client !== null; + } + + /** + * Envía un mensaje al modelo Kimi y obtiene una respuesta + */ + async chat( + messages: KimiMessage[], + options?: KimiChatOptions, + ): Promise { + if (!this.isReady()) { + this.logger.warn('⚠️ Kimi no está disponible, usando fallback'); + return this.getFallbackResponse(messages); + } + + try { + const response = await this.client!.chat.completions.create({ + model: this.model, + messages: messages as any, + temperature: options?.temperature ?? this.defaultTemperature, + max_tokens: options?.maxTokens ?? this.defaultMaxTokens, + top_p: options?.topP ?? this.defaultTopP, + }); + + const content = response.choices?.[0]?.message?.content; + + if (!content) { + throw new Error('Respuesta vacía de Kimi'); + } + + return content; + } catch (error: any) { + this.logger.error(`❌ Error en chat Kimi: ${error.message}`); + + // Manejar errores específicos + if (error.status === 429) { + throw new Error('Límite de rate alcanzado. Intenta de nuevo en unos segundos.'); + } + if (error.status === 401) { + throw new Error('API Key inválida o expirada.'); + } + + // Fallback para otros errores + return this.getFallbackResponse(messages); + } + } + + /** + * Chat con system prompt predefinido para el asistente de viajes + */ + async chatWithTravelAssistant( + userMessage: string, + conversationHistory: KimiMessage[] = [], + language: string = 'es', + ): Promise { + const systemPrompt = this.getTravelAssistantPrompt(language); + + const messages: KimiMessage[] = [ + { role: 'system', content: systemPrompt }, + ...conversationHistory, + { role: 'user', content: userMessage }, + ]; + + return this.chat(messages); + } + + /** + * Genera un itinerario personalizado + */ + async generateItinerary(params: { + destination: string; + days: number; + interests: string[]; + budget: string; + language: string; + }): Promise { + const { destination, days, interests, budget, language } = params; + + const prompt = language === 'es' + ? `Crea un itinerario detallado de ${days} días para ${destination}. + +Intereses del viajero: ${interests.join(', ')} +Presupuesto: ${budget} + +El itinerario debe incluir: +- Actividades por día con horarios específicos +- Restaurantes recomendados con precios aproximados +- Tips de transporte +- Presupuesto estimado por día + +Formato con emojis y estructura clara por días.` + : `Create a detailed ${days}-day itinerary for ${destination}. + +Traveler interests: ${interests.join(', ')} +Budget: ${budget} + +The itinerary should include: +- Daily activities with specific times +- Recommended restaurants with approximate prices +- Transport tips +- Estimated daily budget + +Format with emojis and clear day-by-day structure.`; + + const systemPrompt = this.getTravelAssistantPrompt(language); + + return this.chat([ + { role: 'system', content: systemPrompt }, + { role: 'user', content: prompt }, + ], { + maxTokens: 8192, // Itinerarios necesitan más tokens + }); + } + + /** + * Procesa una imagen para reconocimiento de monumentos + * Nota: Kimi 2.5 es multimodal, puede procesar imágenes + */ + async processImage( + imageUrl: string, + query: string = 'What is this monument or place?', + language: string = 'en', + ): Promise { + if (!this.isReady()) { + return language === 'es' + ? 'Lo siento, el servicio de reconocimiento de imágenes no está disponible en este momento.' + : 'Sorry, the image recognition service is not available at the moment.'; + } + + try { + const response = await this.client!.chat.completions.create({ + model: this.model, + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: query }, + { type: 'image_url', image_url: { url: imageUrl } }, + ], + }, + ] as any, + max_tokens: 2048, + }); + + return response.choices?.[0]?.message?.content || 'No se pudo procesar la imagen.'; + } catch (error: any) { + this.logger.error(`❌ Error procesando imagen: ${error.message}`); + return language === 'es' + ? 'Hubo un error al procesar la imagen. Por favor, intenta de nuevo.' + : 'There was an error processing the image. Please try again.'; + } + } + +/** + * System prompt para el asistente de viajes - CON UBICACIÓN + */ + private getTravelAssistantPrompt(language: string = 'es'): string { + const isSpanish = language === 'es'; + + return isSpanish ? `Eres "Karibeo AI", un asistente experto y amigable especializado en viajes a República Dominicana y el Caribe. + +**REGLA CRÍTICA DE UBICACIÓN:** +- Si el usuario proporciona coordenadas GPS o ubicación, SOLO recomienda lugares CERCANOS a esa ubicación (menos de 30 minutos en carro) +- NO recomiendes lugares a horas de distancia +- Ejemplo: Si el usuario está en Santo Domingo, NO menciones Punta Cana, Bávaro, Samaná o Puerto Plata (están a 2-4 horas) +- Ejemplo: Si el usuario está en Santo Domingo, recomienda: Zona Colonial, Malecón, Los Tres Ojos, Boca Chica, Juan Dolio +- SIEMPRE indica la distancia aproximada desde la ubicación del usuario + +**REGLA DE CONVERSACIÓN NATURAL:** +- Para saludos simples (Hola, Hi, Buenos días, etc.) responde con un saludo breve y amigable, NO con una lista de recomendaciones +- Ejemplo: "Hola" → "¡Hola! Soy Karibeo AI, tu guía de viajes en República Dominicana. ¿En qué puedo ayudarte hoy?" +- Solo da recomendaciones detalladas cuando el usuario PREGUNTE específicamente por lugares, hoteles, restaurantes, etc. +- Mantén respuestas cortas para preguntas simples +- Da respuestas detalladas solo cuando se pida información específica + +REGLAS DE IDIOMA: +- Responde SIEMPRE en el mismo idioma que el usuario +- Si el usuario escribe en español → responde en español +- Si el usuario escribe en inglés → responde en inglés + +CONOCIMIENTO POR ZONA: + +**SANTO DOMINGO Y ALREDEDORES (para usuarios en Santo Domingo):** +- Zona Colonial: Catedral Primada, Alcázar de Colón, Calle Las Damas, Parque Colón +- Malecón de Santo Domingo +- Los Tres Ojos (cuevas) +- Faro a Colón +- Jardín Botánico Nacional +- Playas cercanas: Boca Chica (30 min), Juan Dolio (45 min), Guayacanes (40 min) +- Gastronomía: Pat'e Palo, Mesón de Bari, Adrian Tropical, El Conuco + +**PUNTA CANA / BÁVARO (solo si el usuario está ahí):** +- Playa Bávaro, Playa Macao +- Isla Saona, Isla Catalina +- Cap Cana, Hoyo Azul + +**SAMANÁ (solo si el usuario está ahí):** +- Playa Rincón, El Limón, Cayo Levantado +- Observación de ballenas (temporada) + +**NORTE - Puerto Plata (solo si el usuario está ahí):** +- Teleférico, Playa Dorada +- 27 Charcos de Damajagua + +**JARABACOA/CONSTANZA (solo si el usuario está ahí):** +- Pico Duarte, rafting, Salto de Jimenoa + +CAPACIDADES: +1. Crear itinerarios personalizados RESPETANDO la ubicación del usuario +2. Recomendar restaurantes cercanos con precios específicos +3. Calcular presupuestos de viaje +4. Dar tips de seguridad y transporte local +5. Sugerir experiencias locales auténticas CERCANAS + +PERSONALIDAD: +- Entusiasta y apasionado por el Caribe +- Usa emojis relevantes: 🏖️ 🥥 💃 🎵 🏛️ 🍽️ +- Da recomendaciones específicas con nombres reales +- SIEMPRE indica distancia y tiempo de viaje desde la ubicación del usuario +- Menciona precios en USD y RD$ cuando sea relevante + +FORMATO DE RESPUESTAS: +- Usa listas con emojis para organizar información +- Incluye horarios y precios cuando sea posible +- Da tips prácticos de transporte +- Indica tiempo de llegada desde la ubicación del usuario + +RESTRICCIONES: +- Solo responde sobre viajes, turismo y destinos +- Si preguntan sobre otros temas, redirige amablemente a viajes +- NO recomiendes lugares lejanos sin mencionar que están lejos +- Promueve turismo sostenible y respeto por la cultura local` + : `You are "Karibeo AI", a friendly expert travel assistant specialized in the Dominican Republic and the Caribbean. + +**CRITICAL LOCATION RULE:** +- If the user provides GPS coordinates or location, ONLY recommend places NEARBY to that location (less than 30 minutes by car) +- DO NOT recommend places hours away +- Example: If user is in Santo Domingo, DO NOT mention Punta Cana, Bávaro, Samaná or Puerto Plata (they are 2-4 hours away) +- Example: If user is in Santo Domingo, recommend: Colonial Zone, Malecón, Los Tres Ojos, Boca Chica, Juan Dolio +- ALWAYS indicate approximate distance from the user's location + +**NATURAL CONVERSATION RULE:** +- For simple greetings (Hello, Hi, Good morning, etc.) respond with a brief friendly greeting, NOT a list of recommendations +- Example: "Hello" → "Hi! I'm Karibeo AI, your travel guide for the Dominican Republic. How can I help you today?" +- Only give detailed recommendations when the user SPECIFICALLY asks for places, hotels, restaurants, etc. +- Keep responses short for simple questions +- Give detailed responses only when specific information is requested + +LANGUAGE RULES: +- ALWAYS respond in the same language as the user +- If user writes in Spanish → respond in Spanish +- If user writes in English → respond in English + +KNOWLEDGE BY ZONE: + +**SANTO DOMINGO AND SURROUNDINGS (for users in Santo Domingo):** +- Colonial Zone: Primera Cathedral, Alcázar de Colón, Las Damas Street, Colón Park +- Santo Domingo Malecón +- Los Tres Ojos (caves) +- Columbus Lighthouse +- National Botanical Garden +- Nearby beaches: Boca Chica (30 min), Juan Dolio (45 min), Guayacanes (40 min) +- Gastronomy: Pat'e Palo, Mesón de Bari, Adrian Tropical, El Conuco + +**PUNTA CANA / BÁVARO (only if user is there):** +- Bávaro Beach, Macao Beach +- Saona Island, Catalina Island +- Cap Cana, Hoyo Azul + +**SAMANÁ (only if user is there):** +- Rincón Beach, El Limón, Cayo Levantado +- Whale watching (season) + +**NORTH - Puerto Plata (only if user is there):** +- Cable car, Playa Dorada +- 27 Waterfalls of Damajagua + +**JARABACOA/CONSTANZA (only if user is there):** +- Pico Duarte, rafting, Jimenoa Waterfall + +CAPABILITIES: +1. Create personalized itineraries RESPECTING the user's location +2. Recommend nearby restaurants with specific prices +3. Calculate travel budgets +4. Provide safety and local transport tips +5. Suggest NEARBY authentic local experiences + +PERSONALITY: +- Enthusiastic and passionate about the Caribbean +- Use relevant emojis: 🏖️ 🥥 💃 🎵 🏛️ 🍽️ +- Give specific recommendations with real names +- ALWAYS indicate distance and travel time from user's location +- Mention prices in USD and RD$ when relevant + +RESPONSE FORMAT: +- Use lists with emojis to organize information +- Include schedules and prices when possible +- Give practical transport tips +- Indicate arrival time from user's location + +RESTRICTIONS: +- Only respond about travel, tourism and destinations +- If asked about other topics, kindly redirect to travel +- DO NOT recommend distant places without mentioning they are far +- Promote sustainable tourism and respect for local culture`; + } + + /** + * Respuesta de fallback cuando Kimi no está disponible + */ + private getFallbackResponse(messages: KimiMessage[]): string { + const lastUserMessage = messages.filter(m => m.role === 'user').pop()?.content.toLowerCase() || ''; + + // Detectar idioma + const isSpanish = /[áéíóúñ¿¡]/.test(lastUserMessage) || + lastUserMessage.includes('hola') || + lastUserMessage.includes('quiero') || + lastUserMessage.includes('donde'); + + if (isSpanish) { + if (lastUserMessage.includes('itinerario') || lastUserMessage.includes('plan') || lastUserMessage.includes('días')) { + return `¡Perfecto! Me encantaría ayudarte a planificar tu viaje + +Para crear el mejor itinerario, cuéntame: +📅 ¿Cuántos días estarás? +💰 ¿Cuál es tu presupuesto aproximado? +🎯 ¿Qué te interesa más: cultura, playa, gastronomía o aventura? + +Mientras tanto, aquí tienes algunas ideas: + +**Santo Domingo** (Zona Colonial) +- Catedral Primada de América ⭐ 4.9/5 +- Alcázar de Colón ⭐ 4.8/5 +- Restaurantes: Pat'e Palo, Mesón de Bari + +**Punta Cana** (Playas) +- Playa Bávaro - Las mejores playas del Caribe +- Excursión a Isla Saona + +¡Cuéntame más y te preparo algo increíble! 🌴`; + } + + if (lastUserMessage.includes('restaurante') || lastUserMessage.includes('comer') || lastUserMessage.includes('comida')) { + return `¡Aquí están mis recomendaciones gastronómicas! 🍽️ + +**ZONA COLONIAL** +📍 Pat'e Palo European Brasserie ⭐ 4.6/5 + 💰 $55 USD promedio | Fine dining + +📍 Mesón de Bari ⭐ 4.7/5 + 💰 $30 USD promedio | Español-Dominicano + +**MALECÓN** +📍 Adrian Tropical ⭐ 4.4/5 + 💰 $25 USD promedio | Mariscos frescos + +**COCINA DOMINICANA AUTÉNTICA** +📍 El Conuco ⭐ 4.5/5 + 💰 $35 USD promedio | Mofongo increíble + +¿Qué tipo de comida prefieres? 😊`; + } + + return `¡Hola! Soy Karibeo AI ✨ + +Soy tu guía experto para República Dominicana y el Caribe. Puedo ayudarte con: + +📋 **Crear itinerarios** - "Créame un plan de 3 días" +🍽️ **Restaurantes** - "Dónde comer en Santo Domingo" +🏖️ **Playas** - "Mejores playas cerca de la capital" +💰 **Presupuestos** - "Cuánto cuesta visitar Punta Cana" +🏨 **Hoteles** - "Hoteles recomendados en Zona Colonial" + +¿En qué puedo ayudarte hoy?`; + } + + // English fallback + return `Hello! I'm Karibeo AI ✨ + +I'm your expert guide for the Dominican Republic and the Caribbean. I can help you with: + +📋 **Itineraries** - "Create a 3-day plan" +🍽️ **Restaurants** - "Where to eat in Santo Domingo" +🏖️ **Beaches** - "Best beaches near the capital" +💰 **Budgets** - "How much to visit Punta Cana" +🏨 **Hotels** - "Recommended hotels in Colonial Zone" + +How can I help you today?`; + } +} diff --git a/src/modules/kimi/kimi.service.ts.backup b/src/modules/kimi/kimi.service.ts.backup new file mode 100644 index 0000000..871074c --- /dev/null +++ b/src/modules/kimi/kimi.service.ts.backup @@ -0,0 +1,399 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import OpenAI from 'openai'; + +export interface KimiMessage { + role: 'system' | 'user' | 'assistant'; + content: string; +} + +export interface KimiChatOptions { + temperature?: number; + maxTokens?: number; + topP?: number; + stream?: boolean; +} + +@Injectable() +export class KimiService implements OnModuleInit { + private readonly logger = new Logger(KimiService.name); + private client: OpenAI | null = null; + private model: string; + private defaultTemperature: number; + private defaultMaxTokens: number; + private defaultTopP: number; + private isConfigured: boolean = false; + + constructor(private readonly configService: ConfigService) { + this.model = this.configService.get('kimi.model') || 'kimi-k2.5'; + this.defaultTemperature = this.configService.get('kimi.temperature') || 0.6; + this.defaultMaxTokens = this.configService.get('kimi.maxTokens') || 4096; + this.defaultTopP = this.configService.get('kimi.topP') || 0.95; + } + + async onModuleInit() { + await this.initialize(); + } + + private async initialize(): Promise { + const apiKey = this.configService.get('kimi.apiKey'); + const baseUrl = this.configService.get('kimi.baseUrl'); + + if (!apiKey) { + this.logger.warn('⚠️ KIMI_API_KEY no está configurada. El servicio de IA usará respuestas de fallback.'); + this.isConfigured = false; + return; + } + + try { + this.client = new OpenAI({ + apiKey, + baseURL: baseUrl || 'https://api.moonshot.ai/v1', + }); + + // Test de conexión + this.logger.log('🔧 Inicializando Kimi Service...'); + this.logger.log(`📌 Base URL: ${baseUrl}`); + this.logger.log(`📌 Model: ${this.model}`); + + // Verificar conexión con un mensaje simple + const testResponse = await this.client.chat.completions.create({ + model: this.model, + messages: [{ role: 'user', content: 'Hello' }], + max_tokens: 10, + }); + + if (testResponse.choices?.[0]?.message?.content) { + this.logger.log('✅ Kimi Service inicializado correctamente'); + this.isConfigured = true; + } + } catch (error: any) { + this.logger.error(`❌ Error inicializando Kimi: ${error.message}`); + this.isConfigured = false; + } + } + + /** + * Verifica si el servicio está configurado y funcionando + */ + isReady(): boolean { + return this.isConfigured && this.client !== null; + } + + /** + * Envía un mensaje al modelo Kimi y obtiene una respuesta + */ + async chat( + messages: KimiMessage[], + options?: KimiChatOptions, + ): Promise { + if (!this.isReady()) { + this.logger.warn('⚠️ Kimi no está disponible, usando fallback'); + return this.getFallbackResponse(messages); + } + + try { + const response = await this.client!.chat.completions.create({ + model: this.model, + messages: messages as any, + temperature: options?.temperature ?? this.defaultTemperature, + max_tokens: options?.maxTokens ?? this.defaultMaxTokens, + top_p: options?.topP ?? this.defaultTopP, + }); + + const content = response.choices?.[0]?.message?.content; + + if (!content) { + throw new Error('Respuesta vacía de Kimi'); + } + + return content; + } catch (error: any) { + this.logger.error(`❌ Error en chat Kimi: ${error.message}`); + + // Manejar errores específicos + if (error.status === 429) { + throw new Error('Límite de rate alcanzado. Intenta de nuevo en unos segundos.'); + } + if (error.status === 401) { + throw new Error('API Key inválida o expirada.'); + } + + // Fallback para otros errores + return this.getFallbackResponse(messages); + } + } + + /** + * Chat con system prompt predefinido para el asistente de viajes + */ + async chatWithTravelAssistant( + userMessage: string, + conversationHistory: KimiMessage[] = [], + language: string = 'es', + ): Promise { + const systemPrompt = this.getTravelAssistantPrompt(language); + + const messages: KimiMessage[] = [ + { role: 'system', content: systemPrompt }, + ...conversationHistory, + { role: 'user', content: userMessage }, + ]; + + return this.chat(messages); + } + + /** + * Genera un itinerario personalizado + */ + async generateItinerary(params: { + destination: string; + days: number; + interests: string[]; + budget: string; + language: string; + }): Promise { + const { destination, days, interests, budget, language } = params; + + const prompt = language === 'es' + ? `Crea un itinerario detallado de ${days} días para ${destination}. + +Intereses del viajero: ${interests.join(', ')} +Presupuesto: ${budget} + +El itinerario debe incluir: +- Actividades por día con horarios específicos +- Restaurantes recomendados con precios aproximados +- Tips de transporte +- Presupuesto estimado por día + +Formato con emojis y estructura clara por días.` + : `Create a detailed ${days}-day itinerary for ${destination}. + +Traveler interests: ${interests.join(', ')} +Budget: ${budget} + +The itinerary should include: +- Daily activities with specific times +- Recommended restaurants with approximate prices +- Transport tips +- Estimated daily budget + +Format with emojis and clear day-by-day structure.`; + + const systemPrompt = this.getTravelAssistantPrompt(language); + + return this.chat([ + { role: 'system', content: systemPrompt }, + { role: 'user', content: prompt }, + ], { + maxTokens: 8192, // Itinerarios necesitan más tokens + }); + } + + /** + * Procesa una imagen para reconocimiento de monumentos + * Nota: Kimi 2.5 es multimodal, puede procesar imágenes + */ + async processImage( + imageUrl: string, + query: string = 'What is this monument or place?', + language: string = 'en', + ): Promise { + if (!this.isReady()) { + return language === 'es' + ? 'Lo siento, el servicio de reconocimiento de imágenes no está disponible en este momento.' + : 'Sorry, the image recognition service is not available at the moment.'; + } + + try { + const response = await this.client!.chat.completions.create({ + model: this.model, + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: query }, + { type: 'image_url', image_url: { url: imageUrl } }, + ], + }, + ] as any, + max_tokens: 2048, + }); + + return response.choices?.[0]?.message?.content || 'No se pudo procesar la imagen.'; + } catch (error: any) { + this.logger.error(`❌ Error procesando imagen: ${error.message}`); + return language === 'es' + ? 'Hubo un error al procesar la imagen. Por favor, intenta de nuevo.' + : 'There was an error processing the image. Please try again.'; + } + } + + /** + * System prompt para el asistente de viajes + */ + private getTravelAssistantPrompt(language: string = 'es'): string { + const isSpanish = language === 'es'; + + return isSpanish ? `Eres "Karibeo AI", un asistente experto y amigable especializado en viajes a República Dominicana y el Caribe, con énfasis en Santo Domingo. + +REGLAS DE IDIOMA: +- Responde SIEMPRE en el mismo idioma que el usuario +- Si el usuario escribe en español → responde en español +- Si el usuario escribe en inglés → responde en inglés + +BASE DE CONOCIMIENTO: +- Especialista en República Dominicana: Zona Colonial, Malecón, Punta Cana, Samaná, Puerto Plata, Jarabacoa +- Conocimiento profundo de gastronomía dominicana: mangú, sancocho, mofongo, chimichurri dominicano +- Cultura: merengue, bachata, carnaval, historia colonial +- Playas: Playa Bávaro, Playa Rincón, Bahía de las Águilas, Cayo Arena + +CAPACIDADES: +1. Crear itinerarios personalizados día a día +2. Recomendar restaurantes con precios específicos +3. Calcular presupuestos de viaje +4. Dar tips de seguridad y transporte +5. Sugerir experiencias locales auténticas + +PERSONALIDAD: +- Entusiasta y apasionado por el Caribe +- Usa emojis relevantes: 🏖️ 🥥 💃 🎵 🏛️ 🍽️ +- Da recomendaciones específicas con nombres reales +- Incluye tips locales que solo los dominicanos conocen +- Menciona precios en USD y RD$ cuando sea relevante + +FORMATO DE RESPUESTAS: +- Usa listas con emojis para organizar información +- Incluye horarios y precios cuando sea posible +- Da tips prácticos de transporte +- Sugiere el mejor momento para visitar cada lugar + +RESTRICCIONES: +- Solo responde sobre viajes, turismo y destinos +- Si preguntan sobre otros temas, redirige amablemente a viajes +- Sé honesto si no tienes información específica +- Promueve turismo sostenible y respeto por la cultura local` + : `You are "Karibeo AI", a friendly expert travel assistant specialized in the Dominican Republic and the Caribbean, with emphasis on Santo Domingo. + +LANGUAGE RULES: +- ALWAYS respond in the same language as the user +- If user writes in Spanish → respond in Spanish +- If user writes in English → respond in English + +KNOWLEDGE BASE: +- Dominican Republic specialist: Colonial Zone, Malecón, Punta Cana, Samaná, Puerto Plata, Jarabacoa +- Deep knowledge of Dominican cuisine: mangú, sancocho, mofongo, Dominican chimichurri +- Culture: merengue, bachata, carnival, colonial history +- Beaches: Playa Bávaro, Playa Rincón, Bahía de las Águilas, Cayo Arena + +CAPABILITIES: +1. Create personalized day-by-day itineraries +2. Recommend restaurants with specific prices +3. Calculate travel budgets +4. Provide safety and transport tips +5. Suggest authentic local experiences + +PERSONALITY: +- Enthusiastic and passionate about the Caribbean +- Use relevant emojis: 🏖️ 🥥 💃 🎵 🏛️ 🍽️ +- Give specific recommendations with real names +- Include local tips only Dominicans know +- Mention prices in USD and RD$ when relevant + +RESPONSE FORMAT: +- Use lists with emojis to organize information +- Include schedules and prices when possible +- Give practical transport tips +- Suggest the best time to visit each place + +RESTRICTIONS: +- Only respond about travel, tourism and destinations +- If asked about other topics, kindly redirect to travel +- Be honest if you don't have specific information +- Promote sustainable tourism and respect for local culture`; + } + + /** + * Respuesta de fallback cuando Kimi no está disponible + */ + private getFallbackResponse(messages: KimiMessage[]): string { + const lastUserMessage = messages.filter(m => m.role === 'user').pop()?.content.toLowerCase() || ''; + + // Detectar idioma + const isSpanish = /[áéíóúñ¿¡]/.test(lastUserMessage) || + lastUserMessage.includes('hola') || + lastUserMessage.includes('quiero') || + lastUserMessage.includes('donde'); + + if (isSpanish) { + if (lastUserMessage.includes('itinerario') || lastUserMessage.includes('plan') || lastUserMessage.includes('días')) { + return `¡Perfecto! Me encantaría ayudarte a planificar tu viaje 🇩🇴 + +Para crear el mejor itinerario, cuéntame: +📅 ¿Cuántos días estarás? +💰 ¿Cuál es tu presupuesto aproximado? +🎯 ¿Qué te interesa más: cultura, playa, gastronomía o aventura? + +Mientras tanto, aquí tienes algunas ideas: + +**Santo Domingo** (Zona Colonial) +- Catedral Primada de América ⭐ 4.9/5 +- Alcázar de Colón ⭐ 4.8/5 +- Restaurantes: Pat'e Palo, Mesón de Bari + +**Punta Cana** (Playas) +- Playa Bávaro - Las mejores playas del Caribe +- Excursión a Isla Saona + +¡Cuéntame más y te preparo algo increíble! 🌴`; + } + + if (lastUserMessage.includes('restaurante') || lastUserMessage.includes('comer') || lastUserMessage.includes('comida')) { + return `¡Aquí están mis recomendaciones gastronómicas! 🍽️🇩🇴 + +**ZONA COLONIAL** +📍 Pat'e Palo European Brasserie ⭐ 4.6/5 + 💰 $55 USD promedio | Fine dining + +📍 Mesón de Bari ⭐ 4.7/5 + 💰 $30 USD promedio | Español-Dominicano + +**MALECÓN** +📍 Adrian Tropical ⭐ 4.4/5 + 💰 $25 USD promedio | Mariscos frescos + +**COCINA DOMINICANA AUTÉNTICA** +📍 El Conuco ⭐ 4.5/5 + 💰 $35 USD promedio | Mofongo increíble + +¿Qué tipo de comida prefieres? 😊`; + } + + return `¡Hola! Soy Karibeo AI 🇩🇴✨ + +Soy tu guía experto para República Dominicana y el Caribe. Puedo ayudarte con: + +📋 **Crear itinerarios** - "Créame un plan de 3 días" +🍽️ **Restaurantes** - "Dónde comer en Santo Domingo" +🏖️ **Playas** - "Mejores playas cerca de la capital" +💰 **Presupuestos** - "Cuánto cuesta visitar Punta Cana" +🏨 **Hoteles** - "Hoteles recomendados en Zona Colonial" + +¿En qué puedo ayudarte hoy?`; + } + + // English fallback + return `Hello! I'm Karibeo AI 🇩🇴✨ + +I'm your expert guide for the Dominican Republic and the Caribbean. I can help you with: + +📋 **Itineraries** - "Create a 3-day plan" +🍽️ **Restaurants** - "Where to eat in Santo Domingo" +🏖️ **Beaches** - "Best beaches near the capital" +💰 **Budgets** - "How much to visit Punta Cana" +🏨 **Hotels** - "Recommended hotels in Colonial Zone" + +How can I help you today?`; + } +} diff --git a/src/modules/quiz/dto/quiz.dto.ts b/src/modules/quiz/dto/quiz.dto.ts new file mode 100644 index 0000000..8c940b9 --- /dev/null +++ b/src/modules/quiz/dto/quiz.dto.ts @@ -0,0 +1,71 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsOptional, IsString, IsBoolean } from 'class-validator'; + +export class SubmitQuizDto { + @ApiPropertyOptional({ description: 'Travel styles', example: ['adventure', 'cultural'] }) + @IsOptional() + @IsArray() + travelStyles?: string[]; + + @ApiPropertyOptional({ description: 'Preferred activities', example: ['hiking', 'beaches'] }) + @IsOptional() + @IsArray() + preferredActivities?: string[]; + + @ApiPropertyOptional({ description: 'Accommodation preferences', example: ['hotel', 'resort'] }) + @IsOptional() + @IsArray() + accommodationPreferences?: string[]; + + @ApiPropertyOptional({ description: 'Budget range', example: 'mid-range' }) + @IsOptional() + @IsString() + budgetRange?: string; + + @ApiPropertyOptional({ description: 'Trip duration preference', example: 'week' }) + @IsOptional() + @IsString() + tripDuration?: string; + + @ApiPropertyOptional({ description: 'Group type', example: 'couple' }) + @IsOptional() + @IsString() + groupType?: string; + + @ApiPropertyOptional({ description: 'Cuisine preferences', example: ['local', 'seafood'] }) + @IsOptional() + @IsArray() + cuisinePreferences?: string[]; + + @ApiPropertyOptional({ description: 'Interests', example: ['history', 'nature'] }) + @IsOptional() + @IsArray() + interests?: string[]; + + @ApiPropertyOptional({ description: 'Accessibility needs' }) + @IsOptional() + @IsArray() + accessibilityNeeds?: string[]; + + @ApiPropertyOptional({ description: 'Mark quiz as completed', default: false }) + @IsOptional() + @IsBoolean() + isCompleted?: boolean; +} + +export class QuizQuestionsDto { + @ApiProperty({ description: 'Question ID' }) + id: string; + + @ApiProperty({ description: 'Question text' }) + question: string; + + @ApiProperty({ description: 'Question type', example: 'multi-select' }) + type: 'single-select' | 'multi-select' | 'text'; + + @ApiProperty({ description: 'Available options' }) + options: Array<{ value: string; label: string; icon?: string }>; + + @ApiPropertyOptional({ description: 'Maximum selections for multi-select' }) + maxSelections?: number; +} diff --git a/src/modules/quiz/quiz.controller.ts b/src/modules/quiz/quiz.controller.ts new file mode 100644 index 0000000..3ea78d4 --- /dev/null +++ b/src/modules/quiz/quiz.controller.ts @@ -0,0 +1,42 @@ +import { Controller, Get, Post, Body, Delete, UseGuards, Request } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { QuizService } from './quiz.service'; +import { SubmitQuizDto } from './dto/quiz.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { TravelQuizResponse } from '../../entities/travel-quiz.entity'; + +@ApiTags('Quiz') +@Controller('quiz') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class QuizController { + constructor(private readonly quizService: QuizService) {} + + @Get('questions') + @ApiOperation({ summary: 'Get quiz questions' }) + @ApiResponse({ status: 200, description: 'Quiz questions' }) + getQuestions() { + return this.quizService.getQuestions(); + } + + @Get('my') + @ApiOperation({ summary: 'Get my quiz response' }) + @ApiResponse({ status: 200, type: TravelQuizResponse }) + getMyResponse(@Request() req) { + return this.quizService.getMyQuizResponse(req.user.id); + } + + @Post('submit') + @ApiOperation({ summary: 'Submit quiz answers' }) + @ApiResponse({ status: 201, type: TravelQuizResponse }) + submitQuiz(@Body() submitDto: SubmitQuizDto, @Request() req) { + return this.quizService.submitQuiz(req.user.id, submitDto); + } + + @Delete('reset') + @ApiOperation({ summary: 'Reset quiz' }) + @ApiResponse({ status: 200, description: 'Quiz reset' }) + resetQuiz(@Request() req) { + return this.quizService.resetQuiz(req.user.id); + } +} diff --git a/src/modules/quiz/quiz.module.ts b/src/modules/quiz/quiz.module.ts new file mode 100644 index 0000000..aa2fb38 --- /dev/null +++ b/src/modules/quiz/quiz.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { QuizService } from './quiz.service'; +import { QuizController } from './quiz.controller'; +import { TravelQuizResponse } from '../../entities/travel-quiz.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([TravelQuizResponse])], + controllers: [QuizController], + providers: [QuizService], + exports: [QuizService], +}) +export class QuizModule {} diff --git a/src/modules/quiz/quiz.service.ts b/src/modules/quiz/quiz.service.ts new file mode 100644 index 0000000..9e089b2 --- /dev/null +++ b/src/modules/quiz/quiz.service.ts @@ -0,0 +1,135 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { TravelQuizResponse } from '../../entities/travel-quiz.entity'; +import { SubmitQuizDto } from './dto/quiz.dto'; + +@Injectable() +export class QuizService { + constructor( + @InjectRepository(TravelQuizResponse) + private readonly quizRepository: Repository, + ) {} + + getQuestions() { + return [ + { + id: 'travel_styles', + question: 'What type of traveler are you?', + questionEs: 'Que tipo de viajero eres?', + type: 'multi-select', + maxSelections: 3, + options: [ + { value: 'adventure', label: 'Adventure Seeker', icon: 'mountain' }, + { value: 'cultural', label: 'Culture Explorer', icon: 'landmark' }, + { value: 'beach', label: 'Beach Lover', icon: 'umbrella-beach' }, + { value: 'luxury', label: 'Luxury Traveler', icon: 'gem' }, + { value: 'foodie', label: 'Food Enthusiast', icon: 'utensils' }, + { value: 'nature', label: 'Nature Lover', icon: 'leaf' }, + ], + }, + { + id: 'activities', + question: 'What activities interest you?', + type: 'multi-select', + maxSelections: 5, + options: [ + { value: 'hiking', label: 'Hiking', icon: 'hiking' }, + { value: 'diving', label: 'Diving/Snorkeling', icon: 'water' }, + { value: 'nightlife', label: 'Nightlife', icon: 'music' }, + { value: 'shopping', label: 'Shopping', icon: 'shopping-bag' }, + { value: 'spa', label: 'Spa & Wellness', icon: 'spa' }, + { value: 'photography', label: 'Photography', icon: 'camera' }, + { value: 'museums', label: 'Museums', icon: 'museum' }, + ], + }, + { + id: 'budget', + question: 'What is your typical daily budget?', + type: 'single-select', + options: [ + { value: 'budget', label: 'Budget ($50-100/day)', icon: 'dollar-sign' }, + { value: 'mid', label: 'Mid-Range ($100-200/day)', icon: 'coins' }, + { value: 'high', label: 'Upscale ($200-400/day)', icon: 'gem' }, + { value: 'luxury', label: 'Luxury ($400+/day)', icon: 'crown' }, + ], + }, + { + id: 'group_type', + question: 'Who do you usually travel with?', + type: 'single-select', + options: [ + { value: 'solo', label: 'Solo', icon: 'user' }, + { value: 'couple', label: 'Partner', icon: 'heart' }, + { value: 'family', label: 'Family', icon: 'users' }, + { value: 'friends', label: 'Friends', icon: 'users' }, + ], + }, + ]; + } + + async getMyQuizResponse(userId: string): Promise { + return this.quizRepository.findOne({ where: { userId } }); + } + + async submitQuiz(userId: string, submitDto: SubmitQuizDto): Promise { + let response = await this.quizRepository.findOne({ where: { userId } }); + + if (!response) { + response = this.quizRepository.create({ userId }); + } + + if (submitDto.travelStyles) response.travelStyles = submitDto.travelStyles; + if (submitDto.preferredActivities) response.preferredActivities = submitDto.preferredActivities; + if (submitDto.accommodationPreferences) response.accommodationPreferences = submitDto.accommodationPreferences; + if (submitDto.budgetRange) response.budgetRange = submitDto.budgetRange; + if (submitDto.tripDuration) response.tripDuration = submitDto.tripDuration; + if (submitDto.groupType) response.groupType = submitDto.groupType; + if (submitDto.cuisinePreferences) response.cuisinePreferences = submitDto.cuisinePreferences; + if (submitDto.interests) response.interests = submitDto.interests; + if (submitDto.accessibilityNeeds) response.accessibilityNeeds = submitDto.accessibilityNeeds; + + if (submitDto.isCompleted) { + response.isCompleted = true; + response.completedAt = new Date(); + const persona = this.generatePersona(response); + response.travelPersona = persona.type; + response.personaDescription = persona.description; + } + + return this.quizRepository.save(response); + } + + private generatePersona(response: TravelQuizResponse): { type: string; description: string } { + const styles = response.travelStyles || []; + const activities = response.preferredActivities || []; + const budget = response.budgetRange; + + if (styles.includes('adventure') && activities.some(a => ['hiking', 'diving'].includes(a))) { + return { type: 'Adventure Explorer', description: 'You thrive on adrenaline and new experiences.' }; + } + if (styles.includes('luxury') || budget === 'luxury') { + return { type: 'Luxury Connoisseur', description: 'You appreciate the finer things in life.' }; + } + if (styles.includes('cultural') && activities.includes('museums')) { + return { type: 'Culture Enthusiast', description: 'History, art, and local traditions fascinate you.' }; + } + if (styles.includes('foodie')) { + return { type: 'Culinary Explorer', description: 'Food is your passport to new experiences.' }; + } + if (styles.includes('beach') && styles.includes('nature')) { + return { type: 'Nature Seeker', description: 'Natural wonders call to you.' }; + } + if (response.groupType === 'family') { + return { type: 'Family Explorer', description: 'Creating memories with loved ones is your priority.' }; + } + return { type: 'Curious Traveler', description: 'You are open to all kinds of experiences.' }; + } + + async resetQuiz(userId: string): Promise { + const response = await this.quizRepository.findOne({ where: { userId } }); + if (response) { + await this.quizRepository.remove(response); + } + } +} diff --git a/src/modules/trips/dto/create-trip.dto.ts b/src/modules/trips/dto/create-trip.dto.ts new file mode 100644 index 0000000..79f0b5c --- /dev/null +++ b/src/modules/trips/dto/create-trip.dto.ts @@ -0,0 +1,147 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsString, IsOptional, IsBoolean, IsNumber, IsDateString, IsArray, IsEnum, MaxLength } from 'class-validator'; +import { TripStatus } from '../../../entities/user-trip.entity'; + +export class CreateTripDto { + @ApiProperty({ description: 'Trip name', example: 'Summer Vacation in DR' }) + @IsString() + @MaxLength(150) + name: string; + + @ApiPropertyOptional({ description: 'Trip description' }) + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional({ description: 'Cover image URL' }) + @IsOptional() + @IsString() + coverImageUrl?: string; + + @ApiPropertyOptional({ description: 'Start date', example: '2026-06-01' }) + @IsOptional() + @IsDateString() + startDate?: string; + + @ApiPropertyOptional({ description: 'End date', example: '2026-06-07' }) + @IsOptional() + @IsDateString() + endDate?: string; + + @ApiPropertyOptional({ description: 'Destination' }) + @IsOptional() + @IsString() + destination?: string; + + @ApiPropertyOptional({ description: 'Number of travelers', default: 1 }) + @IsOptional() + @IsNumber() + travelersCount?: number; + + @ApiPropertyOptional({ description: 'Estimated budget' }) + @IsOptional() + @IsNumber() + estimatedBudget?: number; + + @ApiPropertyOptional({ description: 'Budget currency', default: 'USD' }) + @IsOptional() + @IsString() + budgetCurrency?: string; + + @ApiPropertyOptional({ description: 'Trip tags' }) + @IsOptional() + @IsArray() + tags?: string[]; + + @ApiPropertyOptional({ description: 'Is public trip', default: false }) + @IsOptional() + @IsBoolean() + isPublic?: boolean; +} + +export class CreateTripDayDto { + @ApiProperty({ description: 'Day number', example: 1 }) + @IsNumber() + dayNumber: number; + + @ApiPropertyOptional({ description: 'Date', example: '2026-06-01' }) + @IsOptional() + @IsDateString() + date?: string; + + @ApiPropertyOptional({ description: 'Day title' }) + @IsOptional() + @IsString() + title?: string; + + @ApiPropertyOptional({ description: 'Notes' }) + @IsOptional() + @IsString() + notes?: string; +} + +export class CreateTripActivityDto { + @ApiPropertyOptional({ description: 'Reference item ID' }) + @IsOptional() + @IsString() + itemId?: string; + + @ApiPropertyOptional({ description: 'Item type' }) + @IsOptional() + @IsString() + itemType?: string; + + @ApiProperty({ description: 'Activity title' }) + @IsString() + @MaxLength(200) + title: string; + + @ApiPropertyOptional({ description: 'Description' }) + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional({ description: 'Start time', example: '09:00' }) + @IsOptional() + @IsString() + startTime?: string; + + @ApiPropertyOptional({ description: 'End time', example: '12:00' }) + @IsOptional() + @IsString() + endTime?: string; + + @ApiPropertyOptional({ description: 'Duration in minutes' }) + @IsOptional() + @IsNumber() + durationMinutes?: number; + + @ApiPropertyOptional({ description: 'Location name' }) + @IsOptional() + @IsString() + locationName?: string; + + @ApiPropertyOptional({ description: 'Location address' }) + @IsOptional() + @IsString() + locationAddress?: string; + + @ApiPropertyOptional({ description: 'Location coordinates' }) + @IsOptional() + locationCoords?: { lat: number; lng: number }; + + @ApiPropertyOptional({ description: 'Estimated cost' }) + @IsOptional() + @IsNumber() + estimatedCost?: number; + + @ApiPropertyOptional({ description: 'Image URL' }) + @IsOptional() + @IsString() + imageUrl?: string; + + @ApiPropertyOptional({ description: 'Notes' }) + @IsOptional() + @IsString() + notes?: string; +} diff --git a/src/modules/trips/dto/index.ts b/src/modules/trips/dto/index.ts new file mode 100644 index 0000000..af64333 --- /dev/null +++ b/src/modules/trips/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-trip.dto'; +export * from './update-trip.dto'; diff --git a/src/modules/trips/dto/update-trip.dto.ts b/src/modules/trips/dto/update-trip.dto.ts new file mode 100644 index 0000000..7f8cee8 --- /dev/null +++ b/src/modules/trips/dto/update-trip.dto.ts @@ -0,0 +1,16 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateTripDto, CreateTripDayDto, CreateTripActivityDto } from './create-trip.dto'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsOptional } from 'class-validator'; +import { TripStatus } from '../../../entities/user-trip.entity'; + +export class UpdateTripDto extends PartialType(CreateTripDto) { + @ApiPropertyOptional({ description: 'Trip status', enum: TripStatus }) + @IsOptional() + @IsEnum(TripStatus) + status?: TripStatus; +} + +export class UpdateTripDayDto extends PartialType(CreateTripDayDto) {} + +export class UpdateTripActivityDto extends PartialType(CreateTripActivityDto) {} diff --git a/src/modules/trips/trips.controller.ts b/src/modules/trips/trips.controller.ts new file mode 100644 index 0000000..9485c74 --- /dev/null +++ b/src/modules/trips/trips.controller.ts @@ -0,0 +1,145 @@ +import { + Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, Request, +} from '@nestjs/common'; +import { + ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam, +} from '@nestjs/swagger'; +import { TripsService } from './trips.service'; +import { CreateTripDto, CreateTripDayDto, CreateTripActivityDto } from './dto/create-trip.dto'; +import { UpdateTripDto, UpdateTripDayDto, UpdateTripActivityDto } from './dto/update-trip.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { UserTrip, TripDay, TripActivity, TripStatus } from '../../entities/user-trip.entity'; + +@ApiTags('Trips') +@Controller('trips') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class TripsController { + constructor(private readonly tripsService: TripsService) {} + + @Post() + @ApiOperation({ summary: 'Create a new trip' }) + @ApiResponse({ status: 201, type: UserTrip }) + create(@Body() createDto: CreateTripDto, @Request() req) { + return this.tripsService.create(req.user.id, createDto); + } + + @Get('my') + @ApiOperation({ summary: 'Get my trips' }) + @ApiQuery({ name: 'status', required: false, enum: TripStatus }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + findMyTrips( + @Request() req, + @Query('status') status?: TripStatus, + @Query('page') page?: number, + @Query('limit') limit?: number, + ) { + return this.tripsService.findAllByUser(req.user.id, status, page, limit); + } + + @Get('my/stats') + @ApiOperation({ summary: 'Get trip statistics' }) + getStats(@Request() req) { + return this.tripsService.getTripStats(req.user.id); + } + + @Get(':id') + @ApiOperation({ summary: 'Get trip by ID' }) + @ApiParam({ name: 'id', type: 'string' }) + findOne(@Param('id') id: string, @Request() req) { + return this.tripsService.findOne(req.user.id, id); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update trip' }) + @ApiParam({ name: 'id', type: 'string' }) + update(@Param('id') id: string, @Body() updateDto: UpdateTripDto, @Request() req) { + return this.tripsService.update(req.user.id, id, updateDto); + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete trip' }) + @ApiParam({ name: 'id', type: 'string' }) + remove(@Param('id') id: string, @Request() req) { + return this.tripsService.remove(req.user.id, id); + } + + // Days + @Post(':tripId/days') + @ApiOperation({ summary: 'Add day to trip' }) + addDay( + @Param('tripId') tripId: string, + @Body() createDto: CreateTripDayDto, + @Request() req, + ) { + return this.tripsService.addDay(req.user.id, tripId, createDto); + } + + @Patch(':tripId/days/:dayId') + @ApiOperation({ summary: 'Update day' }) + updateDay( + @Param('tripId') tripId: string, + @Param('dayId') dayId: string, + @Body() updateDto: UpdateTripDayDto, + @Request() req, + ) { + return this.tripsService.updateDay(req.user.id, tripId, dayId, updateDto); + } + + @Delete(':tripId/days/:dayId') + @ApiOperation({ summary: 'Remove day' }) + removeDay( + @Param('tripId') tripId: string, + @Param('dayId') dayId: string, + @Request() req, + ) { + return this.tripsService.removeDay(req.user.id, tripId, dayId); + } + + // Activities + @Post(':tripId/days/:dayId/activities') + @ApiOperation({ summary: 'Add activity to day' }) + addActivity( + @Param('tripId') tripId: string, + @Param('dayId') dayId: string, + @Body() createDto: CreateTripActivityDto, + @Request() req, + ) { + return this.tripsService.addActivity(req.user.id, tripId, dayId, createDto); + } + + @Patch(':tripId/days/:dayId/activities/:activityId') + @ApiOperation({ summary: 'Update activity' }) + updateActivity( + @Param('tripId') tripId: string, + @Param('dayId') dayId: string, + @Param('activityId') activityId: string, + @Body() updateDto: UpdateTripActivityDto, + @Request() req, + ) { + return this.tripsService.updateActivity(req.user.id, tripId, dayId, activityId, updateDto); + } + + @Delete(':tripId/days/:dayId/activities/:activityId') + @ApiOperation({ summary: 'Remove activity' }) + removeActivity( + @Param('tripId') tripId: string, + @Param('dayId') dayId: string, + @Param('activityId') activityId: string, + @Request() req, + ) { + return this.tripsService.removeActivity(req.user.id, tripId, dayId, activityId); + } + + @Patch(':tripId/days/:dayId/activities/order') + @ApiOperation({ summary: 'Reorder activities in day' }) + reorderActivities( + @Param('tripId') tripId: string, + @Param('dayId') dayId: string, + @Body() body: { activityIds: string[] }, + @Request() req, + ) { + return this.tripsService.reorderActivities(req.user.id, tripId, dayId, body.activityIds); + } +} diff --git a/src/modules/trips/trips.module.ts b/src/modules/trips/trips.module.ts new file mode 100644 index 0000000..8ce5c99 --- /dev/null +++ b/src/modules/trips/trips.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { TripsService } from './trips.service'; +import { TripsController } from './trips.controller'; +import { UserTrip, TripDay, TripActivity } from '../../entities/user-trip.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([UserTrip, TripDay, TripActivity])], + controllers: [TripsController], + providers: [TripsService], + exports: [TripsService], +}) +export class TripsModule {} diff --git a/src/modules/trips/trips.service.ts b/src/modules/trips/trips.service.ts new file mode 100644 index 0000000..d5468b8 --- /dev/null +++ b/src/modules/trips/trips.service.ts @@ -0,0 +1,181 @@ +import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { UserTrip, TripDay, TripActivity, TripStatus } from '../../entities/user-trip.entity'; +import { CreateTripDto, CreateTripDayDto, CreateTripActivityDto } from './dto/create-trip.dto'; +import { UpdateTripDto, UpdateTripDayDto, UpdateTripActivityDto } from './dto/update-trip.dto'; + +@Injectable() +export class TripsService { + constructor( + @InjectRepository(UserTrip) + private readonly tripRepository: Repository, + @InjectRepository(TripDay) + private readonly dayRepository: Repository, + @InjectRepository(TripActivity) + private readonly activityRepository: Repository, + ) {} + + async create(userId: string, createDto: CreateTripDto): Promise { + const trip = this.tripRepository.create({ + ...createDto, + userId, + status: TripStatus.PLANNING, + }); + return this.tripRepository.save(trip); + } + + async findAllByUser( + userId: string, + status?: TripStatus, + page = 1, + limit = 20, + ): Promise<{ data: UserTrip[]; total: number }> { + const where: any = { userId }; + if (status) where.status = status; + + const [data, total] = await this.tripRepository.findAndCount({ + where, + relations: ['days', 'days.activities'], + order: { updatedAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, + }); + + return { data, total }; + } + + async findOne(userId: string, id: string): Promise { + const trip = await this.tripRepository.findOne({ + where: { id }, + relations: ['days', 'days.activities'], + }); + + if (!trip) throw new NotFoundException('Trip not found'); + if (trip.userId !== userId && !trip.isPublic) { + throw new ForbiddenException('Access denied'); + } + + if (trip.days) { + trip.days.sort((a, b) => a.dayNumber - b.dayNumber); + trip.days.forEach(day => { + if (day.activities) { + day.activities.sort((a, b) => a.sortOrder - b.sortOrder); + } + }); + } + + return trip; + } + + async update(userId: string, id: string, updateDto: UpdateTripDto): Promise { + const trip = await this.tripRepository.findOne({ where: { id, userId } }); + if (!trip) throw new NotFoundException('Trip not found'); + + Object.assign(trip, updateDto); + return this.tripRepository.save(trip); + } + + async remove(userId: string, id: string): Promise { + const trip = await this.tripRepository.findOne({ where: { id, userId } }); + if (!trip) throw new NotFoundException('Trip not found'); + await this.tripRepository.remove(trip); + } + + async addDay(userId: string, tripId: string, createDto: CreateTripDayDto): Promise { + const trip = await this.tripRepository.findOne({ where: { id: tripId, userId } }); + if (!trip) throw new NotFoundException('Trip not found'); + + const day = this.dayRepository.create({ ...createDto, tripId }); + return this.dayRepository.save(day); + } + + async updateDay(userId: string, tripId: string, dayId: string, updateDto: UpdateTripDayDto): Promise { + const trip = await this.tripRepository.findOne({ where: { id: tripId, userId } }); + if (!trip) throw new NotFoundException('Trip not found'); + + const day = await this.dayRepository.findOne({ where: { id: dayId, tripId } }); + if (!day) throw new NotFoundException('Day not found'); + + Object.assign(day, updateDto); + return this.dayRepository.save(day); + } + + async removeDay(userId: string, tripId: string, dayId: string): Promise { + const trip = await this.tripRepository.findOne({ where: { id: tripId, userId } }); + if (!trip) throw new NotFoundException('Trip not found'); + + const day = await this.dayRepository.findOne({ where: { id: dayId, tripId } }); + if (!day) throw new NotFoundException('Day not found'); + + await this.dayRepository.remove(day); + } + + async addActivity(userId: string, tripId: string, dayId: string, createDto: CreateTripActivityDto): Promise { + const trip = await this.tripRepository.findOne({ where: { id: tripId, userId } }); + if (!trip) throw new NotFoundException('Trip not found'); + + const day = await this.dayRepository.findOne({ where: { id: dayId, tripId } }); + if (!day) throw new NotFoundException('Day not found'); + + const maxOrder = await this.activityRepository + .createQueryBuilder('a') + .select('MAX(a.sort_order)', 'max') + .where('a.day_id = :dayId', { dayId }) + .getRawOne(); + + const activity = this.activityRepository.create({ + ...createDto, + dayId, + sortOrder: (maxOrder?.max || 0) + 1, + }); + return this.activityRepository.save(activity); + } + + async updateActivity(userId: string, tripId: string, dayId: string, activityId: string, updateDto: UpdateTripActivityDto): Promise { + const trip = await this.tripRepository.findOne({ where: { id: tripId, userId } }); + if (!trip) throw new NotFoundException('Trip not found'); + + const activity = await this.activityRepository.findOne({ where: { id: activityId, dayId } }); + if (!activity) throw new NotFoundException('Activity not found'); + + Object.assign(activity, updateDto); + return this.activityRepository.save(activity); + } + + async removeActivity(userId: string, tripId: string, dayId: string, activityId: string): Promise { + const trip = await this.tripRepository.findOne({ where: { id: tripId, userId } }); + if (!trip) throw new NotFoundException('Trip not found'); + + const activity = await this.activityRepository.findOne({ where: { id: activityId, dayId } }); + if (!activity) throw new NotFoundException('Activity not found'); + + await this.activityRepository.remove(activity); + } + + async reorderActivities(userId: string, tripId: string, dayId: string, activityIds: string[]): Promise { + const trip = await this.tripRepository.findOne({ where: { id: tripId, userId } }); + if (!trip) throw new NotFoundException('Trip not found'); + + await Promise.all( + activityIds.map((id, index) => + this.activityRepository.update({ id, dayId }, { sortOrder: index }) + ) + ); + } + + async getTripStats(userId: string): Promise> { + const counts = await this.tripRepository + .createQueryBuilder('t') + .select('t.status', 'status') + .addSelect('COUNT(*)', 'count') + .where('t.user_id = :userId', { userId }) + .groupBy('t.status') + .getRawMany(); + + const result: Record = {}; + Object.values(TripStatus).forEach(s => (result[s] = 0)); + counts.forEach(c => (result[c.status] = parseInt(c.count, 10))); + return result as Record; + } +} diff --git a/src/modules/unified-search/index.ts b/src/modules/unified-search/index.ts new file mode 100644 index 0000000..f97ab03 --- /dev/null +++ b/src/modules/unified-search/index.ts @@ -0,0 +1,3 @@ +export * from './unified-search.module'; +export * from './unified-search.service'; +export * from './unified-search.controller'; diff --git a/src/modules/unified-search/unified-search.controller.ts b/src/modules/unified-search/unified-search.controller.ts new file mode 100644 index 0000000..438d5b5 --- /dev/null +++ b/src/modules/unified-search/unified-search.controller.ts @@ -0,0 +1,111 @@ +import { Controller, Get, Post, Body, Query } from '@nestjs/common'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { UnifiedSearchService } from './unified-search.service'; + +@ApiTags('Unified Search') +@Controller({ path: 'search', version: '1' }) +export class UnifiedSearchController { + constructor(private readonly unifiedSearchService: UnifiedSearchService) {} + + @Get('accommodations') + @ApiOperation({ + summary: 'Search accommodations (Karibeo priority, Booking.com complement)', + description: 'Searches accommodations in Karibeo affiliates FIRST, then complements with Booking.com' + }) + async searchAccommodations( + @Query('city') city: string, + @Query('checkin') checkin: string, + @Query('checkout') checkout: string, + @Query('adults') adults: number, + @Query('children') children?: number, + @Query('rooms') rooms?: number, + @Query('includeExternal') includeExternal?: boolean, + ) { + return this.unifiedSearchService.searchAccommodations({ + city, + checkin, + checkout, + adults: Number(adults) || 2, + children: children ? Number(children) : undefined, + rooms: rooms ? Number(rooms) : undefined, + includeExternal: includeExternal !== false, + }); + } + + @Post('accommodations') + @ApiOperation({ summary: 'Search accommodations (POST)' }) + async searchAccommodationsPost(@Body() body: any) { + return this.unifiedSearchService.searchAccommodations({ + ...body, + includeExternal: body.includeExternal !== false, + }); + } + + @Get('flights') + @ApiOperation({ + summary: 'Search flights (Karibeo priority, Booking.com complement)', + description: 'Searches flights in Karibeo affiliates FIRST, then complements with Booking.com' + }) + async searchFlights( + @Query('origin') origin: string, + @Query('destination') destination: string, + @Query('departureDate') departureDate: string, + @Query('returnDate') returnDate?: string, + @Query('adults') adults?: number, + @Query('cabinClass') cabinClass?: string, + @Query('includeExternal') includeExternal?: boolean, + ) { + return this.unifiedSearchService.searchFlights({ + origin, + destination, + departureDate, + returnDate, + adults: Number(adults) || 1, + cabinClass, + includeExternal: includeExternal !== false, + }); + } + + @Post('flights') + @ApiOperation({ summary: 'Search flights (POST)' }) + async searchFlightsPost(@Body() body: any) { + return this.unifiedSearchService.searchFlights({ + ...body, + includeExternal: body.includeExternal !== false, + }); + } + + @Get('vehicles') + @ApiOperation({ + summary: 'Search vehicles/cars (Karibeo priority, Booking.com complement)', + description: 'Searches vehicles in Karibeo affiliates FIRST, then complements with Booking.com' + }) + async searchVehicles( + @Query('pickupLocation') pickupLocation: string, + @Query('dropoffLocation') dropoffLocation?: string, + @Query('pickupDate') pickupDate?: string, + @Query('pickupTime') pickupTime?: string, + @Query('dropoffDate') dropoffDate?: string, + @Query('dropoffTime') dropoffTime?: string, + @Query('includeExternal') includeExternal?: boolean, + ) { + return this.unifiedSearchService.searchVehicles({ + pickupLocation, + dropoffLocation, + pickupDate: pickupDate || new Date().toISOString().split('T')[0], + pickupTime: pickupTime || '10:00', + dropoffDate: dropoffDate || new Date().toISOString().split('T')[0], + dropoffTime: dropoffTime || '10:00', + includeExternal: includeExternal !== false, + }); + } + + @Post('vehicles') + @ApiOperation({ summary: 'Search vehicles (POST)' }) + async searchVehiclesPost(@Body() body: any) { + return this.unifiedSearchService.searchVehicles({ + ...body, + includeExternal: body.includeExternal !== false, + }); + } +} diff --git a/src/modules/unified-search/unified-search.module.ts b/src/modules/unified-search/unified-search.module.ts new file mode 100644 index 0000000..d4af6ec --- /dev/null +++ b/src/modules/unified-search/unified-search.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UnifiedSearchService } from './unified-search.service'; +import { UnifiedSearchController } from './unified-search.controller'; +import { HotelRoom } from '../../entities/hotel-room.entity'; +import { Flight } from '../../entities/flight.entity'; +import { Vehicle } from '../../entities/vehicle.entity'; +import { Listing } from '../../entities/listing.entity'; +import { BookingModule } from '../booking/booking.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([HotelRoom, Flight, Vehicle, Listing]), + BookingModule, + ], + controllers: [UnifiedSearchController], + providers: [UnifiedSearchService], + exports: [UnifiedSearchService], +}) +export class UnifiedSearchModule {} diff --git a/src/modules/unified-search/unified-search.service.ts b/src/modules/unified-search/unified-search.service.ts new file mode 100644 index 0000000..0f5c027 --- /dev/null +++ b/src/modules/unified-search/unified-search.service.ts @@ -0,0 +1,251 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { HotelRoom } from '../../entities/hotel-room.entity'; +import { Flight } from '../../entities/flight.entity'; +import { Vehicle } from '../../entities/vehicle.entity'; +import { Listing } from '../../entities/listing.entity'; +import { BookingService } from '../booking/booking.service'; + +export interface UnifiedSearchResult { + karibeo: T[]; // Resultados de negocios afiliados (PRIORIDAD) + external: T[]; // Resultados de Booking.com (COMPLEMENTO) + source: 'karibeo' | 'booking' | 'mixed'; + totalKaribeo: number; + totalExternal: number; +} + +@Injectable() +export class UnifiedSearchService { + private readonly logger = new Logger(UnifiedSearchService.name); + + constructor( + @InjectRepository(HotelRoom) + private readonly hotelRoomRepository: Repository, + @InjectRepository(Flight) + private readonly flightRepository: Repository, + @InjectRepository(Vehicle) + private readonly vehicleRepository: Repository, + @InjectRepository(Listing) + private readonly listingRepository: Repository, + private readonly bookingService: BookingService, + ) {} + + /** + * Busca alojamientos: PRIMERO Karibeo, SEGUNDO Booking.com + */ + async searchAccommodations(params: { + city: string; + checkin: string; + checkout: string; + adults: number; + children?: number; + rooms?: number; + includeExternal?: boolean; + }): Promise> { + this.logger.log('Searching accommodations - Priority: Karibeo affiliates'); + + // 1. PRIORIDAD: Buscar en Karibeo (listings + hotel rooms) + const karibeoResults = await this.searchKaribeoAccommodations(params); + + let externalResults: any[] = []; + + // 2. COMPLEMENTO: Booking.com (solo si se solicita o hay pocos resultados) + if (params.includeExternal !== false) { + try { + const bookingResponse = await this.bookingService.searchAccommodations({ + city: params.city, + checkin: params.checkin, + checkout: params.checkout, + adults: params.adults, + children: params.children, + rooms: params.rooms, + }); + + if (bookingResponse && Array.isArray(bookingResponse.results)) { + externalResults = bookingResponse.results.map((item: any) => ({ + ...item, + source: 'booking', + isExternal: true, + })); + } + } catch (error) { + this.logger.warn('Booking.com search failed or not configured: ' + error.message); + } + } + + return { + karibeo: karibeoResults, + external: externalResults, + source: karibeoResults.length > 0 ? (externalResults.length > 0 ? 'mixed' : 'karibeo') : 'booking', + totalKaribeo: karibeoResults.length, + totalExternal: externalResults.length, + }; + } + + /** + * Busca vuelos: PRIMERO Karibeo, SEGUNDO Booking.com + */ + async searchFlights(params: { + origin: string; + destination: string; + departureDate: string; + returnDate?: string; + adults: number; + cabinClass?: string; + includeExternal?: boolean; + }): Promise> { + this.logger.log('Searching flights - Priority: Karibeo affiliates'); + + // 1. PRIORIDAD: Buscar vuelos en Karibeo + const karibeoResults = await this.searchKaribeoFlights(params); + + let externalResults: any[] = []; + + // 2. COMPLEMENTO: Booking.com flights + if (params.includeExternal !== false) { + try { + const bookingResponse = await this.bookingService.searchFlights({ + origin: params.origin, + destination: params.destination, + departureDate: params.departureDate, + returnDate: params.returnDate, + adults: params.adults, + cabinClass: params.cabinClass as any, + }); + + if (bookingResponse && Array.isArray(bookingResponse.results)) { + externalResults = bookingResponse.results.map((item: any) => ({ + ...item, + source: 'booking', + isExternal: true, + })); + } + } catch (error) { + this.logger.warn('Booking.com flights search failed: ' + error.message); + } + } + + return { + karibeo: karibeoResults, + external: externalResults, + source: karibeoResults.length > 0 ? (externalResults.length > 0 ? 'mixed' : 'karibeo') : 'booking', + totalKaribeo: karibeoResults.length, + totalExternal: externalResults.length, + }; + } + + /** + * Busca vehículos: PRIMERO Karibeo, SEGUNDO Booking.com + */ + async searchVehicles(params: { + pickupLocation: string; + dropoffLocation?: string; + pickupDate: string; + pickupTime: string; + dropoffDate: string; + dropoffTime: string; + includeExternal?: boolean; + }): Promise> { + this.logger.log('Searching vehicles - Priority: Karibeo affiliates'); + + // 1. PRIORIDAD: Buscar vehículos en Karibeo + const karibeoResults = await this.searchKaribeoVehicles(params); + + let externalResults: any[] = []; + + // 2. COMPLEMENTO: Booking.com cars + if (params.includeExternal !== false) { + try { + const bookingResponse = await this.bookingService.searchCars({ + pickupLocation: params.pickupLocation, + dropoffLocation: params.dropoffLocation, + pickupDate: params.pickupDate, + pickupTime: params.pickupTime, + dropoffDate: params.dropoffDate, + dropoffTime: params.dropoffTime, + }); + + if (bookingResponse && Array.isArray(bookingResponse.results)) { + externalResults = bookingResponse.results.map((item: any) => ({ + ...item, + source: 'booking', + isExternal: true, + })); + } + } catch (error) { + this.logger.warn('Booking.com cars search failed: ' + error.message); + } + } + + return { + karibeo: karibeoResults, + external: externalResults, + source: karibeoResults.length > 0 ? (externalResults.length > 0 ? 'mixed' : 'karibeo') : 'booking', + totalKaribeo: karibeoResults.length, + totalExternal: externalResults.length, + }; + } + + // ==================== KARIBEO INTERNAL SEARCHES ==================== + + private async searchKaribeoAccommodations(params: any): Promise { + const results: any[] = []; + + // Buscar en listings (apartamentos, etc.) + try { + const listings = await this.listingRepository + .createQueryBuilder('listing') + .where('listing.status = :status', { status: 'active' }) + .andWhere('listing.listingType IN (:...types)', { types: ['apartment', 'hotel', 'villa', 'room'] }) + .andWhere('LOWER(listing.title) LIKE :city OR LOWER(listing.description) LIKE :city', + { city: '%' + params.city.toLowerCase() + '%' }) + .getMany(); + results.push(...listings.map(l => ({ ...l, source: 'karibeo', type: 'listing', isExternal: false }))); + } catch (error) { + this.logger.warn('Listings search failed: ' + error.message); + } + + // Buscar habitaciones de hotel + try { + const hotelRooms = await this.hotelRoomRepository + .createQueryBuilder('room') + .where('room.isAvailable = :available', { available: true }) + .getMany(); + results.push(...hotelRooms.map(r => ({ ...r, source: 'karibeo', type: 'hotelRoom', isExternal: false }))); + } catch (error) { + this.logger.warn('Hotel rooms search failed: ' + error.message); + } + + return results; + } + + private async searchKaribeoFlights(params: any): Promise { + try { + const flights = await this.flightRepository + .createQueryBuilder('flight') + .where('flight.originCode = :origin', { origin: params.origin.toUpperCase() }) + .andWhere('flight.destinationCode = :destination', { destination: params.destination.toUpperCase() }) + .andWhere('DATE(flight.departureTime) = :date', { date: params.departureDate }) + .getMany(); + return flights.map(f => ({ ...f, source: 'karibeo', isExternal: false })); + } catch (error) { + this.logger.warn('Karibeo flights search failed: ' + error.message); + return []; + } + } + + private async searchKaribeoVehicles(params: any): Promise { + try { + const vehicles = await this.vehicleRepository + .createQueryBuilder('vehicle') + .where('vehicle.isAvailable = :available', { available: true }) + .andWhere('vehicle.isVerified = :verified', { verified: true }) + .getMany(); + return vehicles.map(v => ({ ...v, source: 'karibeo', isExternal: false })); + } catch (error) { + this.logger.warn('Karibeo vehicles search failed: ' + error.message); + return []; + } + } +} diff --git a/src/modules/users/dto/create-user.dto.ts b/src/modules/users/dto/create-user.dto.ts index 4a81ea9..4654bfa 100755 --- a/src/modules/users/dto/create-user.dto.ts +++ b/src/modules/users/dto/create-user.dto.ts @@ -18,6 +18,7 @@ export class CreateUserDto { @ApiProperty({ description: 'Last name', example: 'Doe' }) @IsString() lastName: string; +@ApiPropertyOptional({ description: 'Username', example: 'johndoe' }) @IsOptional() @IsString() username?: string; @ApiPropertyOptional({ description: 'Phone number', example: '+1234567890' }) @IsOptional() diff --git a/tts/cache/008b44ec41e132d70bbeb63c4130a465.wav b/tts/cache/008b44ec41e132d70bbeb63c4130a465.wav new file mode 100644 index 0000000..bcd6c70 Binary files /dev/null and b/tts/cache/008b44ec41e132d70bbeb63c4130a465.wav differ diff --git a/tts/cache/015be36f8ad81a5dd400a84169a69168.wav b/tts/cache/015be36f8ad81a5dd400a84169a69168.wav new file mode 100644 index 0000000..5750979 Binary files /dev/null and b/tts/cache/015be36f8ad81a5dd400a84169a69168.wav differ diff --git a/tts/cache/01cf0cdec4a5039029a7bcbbc41f6fad.wav b/tts/cache/01cf0cdec4a5039029a7bcbbc41f6fad.wav new file mode 100644 index 0000000..5c3134a Binary files /dev/null and b/tts/cache/01cf0cdec4a5039029a7bcbbc41f6fad.wav differ diff --git a/tts/cache/01e35b4750b630493e5641ab0b2cbf27.wav b/tts/cache/01e35b4750b630493e5641ab0b2cbf27.wav new file mode 100644 index 0000000..84eaa79 Binary files /dev/null and b/tts/cache/01e35b4750b630493e5641ab0b2cbf27.wav differ diff --git a/tts/cache/01fe8aad43c6413b54aef631ce82cbf9.wav b/tts/cache/01fe8aad43c6413b54aef631ce82cbf9.wav new file mode 100644 index 0000000..34109b4 Binary files /dev/null and b/tts/cache/01fe8aad43c6413b54aef631ce82cbf9.wav differ diff --git a/tts/cache/03065a91259bcca13d4c54de7c477eba.wav b/tts/cache/03065a91259bcca13d4c54de7c477eba.wav new file mode 100644 index 0000000..24f45a1 Binary files /dev/null and b/tts/cache/03065a91259bcca13d4c54de7c477eba.wav differ diff --git a/tts/cache/0310c5191e531e744305ef21215f2543.wav b/tts/cache/0310c5191e531e744305ef21215f2543.wav new file mode 100644 index 0000000..4eb1e92 Binary files /dev/null and b/tts/cache/0310c5191e531e744305ef21215f2543.wav differ diff --git a/tts/cache/03342dd481ac2608d479c81dfa6f5fe0.wav b/tts/cache/03342dd481ac2608d479c81dfa6f5fe0.wav new file mode 100644 index 0000000..81e002c Binary files /dev/null and b/tts/cache/03342dd481ac2608d479c81dfa6f5fe0.wav differ diff --git a/tts/cache/03be2230bbe874d38bf35e964ef3bb22.wav b/tts/cache/03be2230bbe874d38bf35e964ef3bb22.wav new file mode 100644 index 0000000..dc03976 Binary files /dev/null and b/tts/cache/03be2230bbe874d38bf35e964ef3bb22.wav differ diff --git a/tts/cache/03fa990fb31197fc641df424bcb50b7b.wav b/tts/cache/03fa990fb31197fc641df424bcb50b7b.wav new file mode 100644 index 0000000..326d931 Binary files /dev/null and b/tts/cache/03fa990fb31197fc641df424bcb50b7b.wav differ diff --git a/tts/cache/05d9b7600cbeed833985a3f3a0129ee6.wav b/tts/cache/05d9b7600cbeed833985a3f3a0129ee6.wav new file mode 100644 index 0000000..4027508 Binary files /dev/null and b/tts/cache/05d9b7600cbeed833985a3f3a0129ee6.wav differ diff --git a/tts/cache/06c94d5f2ace8aa92f3c97eb7dd22f97.wav b/tts/cache/06c94d5f2ace8aa92f3c97eb7dd22f97.wav new file mode 100644 index 0000000..2ce4258 Binary files /dev/null and b/tts/cache/06c94d5f2ace8aa92f3c97eb7dd22f97.wav differ diff --git a/tts/cache/070e3f72fd5419d6c451cad5d2578374.wav b/tts/cache/070e3f72fd5419d6c451cad5d2578374.wav new file mode 100644 index 0000000..6815ec7 Binary files /dev/null and b/tts/cache/070e3f72fd5419d6c451cad5d2578374.wav differ diff --git a/tts/cache/07ad38dffc4ae6e9739cc644669b7def.wav b/tts/cache/07ad38dffc4ae6e9739cc644669b7def.wav new file mode 100644 index 0000000..d381402 Binary files /dev/null and b/tts/cache/07ad38dffc4ae6e9739cc644669b7def.wav differ diff --git a/tts/cache/0843db8bd037ad894c29734b41861621.wav b/tts/cache/0843db8bd037ad894c29734b41861621.wav new file mode 100644 index 0000000..8b6d7a3 Binary files /dev/null and b/tts/cache/0843db8bd037ad894c29734b41861621.wav differ diff --git a/tts/cache/090285cf369694add4ac9329ab8cb692.wav b/tts/cache/090285cf369694add4ac9329ab8cb692.wav new file mode 100644 index 0000000..e821eb8 Binary files /dev/null and b/tts/cache/090285cf369694add4ac9329ab8cb692.wav differ diff --git a/tts/cache/090738243ac1b1326c5cb70a6ac8c43d.wav b/tts/cache/090738243ac1b1326c5cb70a6ac8c43d.wav new file mode 100644 index 0000000..2d4c9d0 Binary files /dev/null and b/tts/cache/090738243ac1b1326c5cb70a6ac8c43d.wav differ diff --git a/tts/cache/098263b697de1f263955601714b1b645.wav b/tts/cache/098263b697de1f263955601714b1b645.wav new file mode 100644 index 0000000..12f6aff Binary files /dev/null and b/tts/cache/098263b697de1f263955601714b1b645.wav differ diff --git a/tts/cache/0a7892b8017770fab9815d2656fe3ce9.wav b/tts/cache/0a7892b8017770fab9815d2656fe3ce9.wav new file mode 100644 index 0000000..0c420de Binary files /dev/null and b/tts/cache/0a7892b8017770fab9815d2656fe3ce9.wav differ diff --git a/tts/cache/0a795d478ebb67fa06ed141341f26bf5.wav b/tts/cache/0a795d478ebb67fa06ed141341f26bf5.wav new file mode 100644 index 0000000..b1a86bb Binary files /dev/null and b/tts/cache/0a795d478ebb67fa06ed141341f26bf5.wav differ diff --git a/tts/cache/0bec3091750629df6be0cff216403900.wav b/tts/cache/0bec3091750629df6be0cff216403900.wav new file mode 100644 index 0000000..416bba0 Binary files /dev/null and b/tts/cache/0bec3091750629df6be0cff216403900.wav differ diff --git a/tts/cache/0cac713c0d08f88ed50fd3a4f294bba7.wav b/tts/cache/0cac713c0d08f88ed50fd3a4f294bba7.wav new file mode 100644 index 0000000..d113b50 Binary files /dev/null and b/tts/cache/0cac713c0d08f88ed50fd3a4f294bba7.wav differ diff --git a/tts/cache/0d34e81450a093435ca76bcf3919074d.wav b/tts/cache/0d34e81450a093435ca76bcf3919074d.wav new file mode 100644 index 0000000..4c91507 Binary files /dev/null and b/tts/cache/0d34e81450a093435ca76bcf3919074d.wav differ diff --git a/tts/cache/0d8cc452e27f7d7ceeae8b9841e109fb.wav b/tts/cache/0d8cc452e27f7d7ceeae8b9841e109fb.wav new file mode 100644 index 0000000..068e82c Binary files /dev/null and b/tts/cache/0d8cc452e27f7d7ceeae8b9841e109fb.wav differ diff --git a/tts/cache/0f831c23158cf948b74f2af5c4da82d7.wav b/tts/cache/0f831c23158cf948b74f2af5c4da82d7.wav new file mode 100644 index 0000000..a5c3a92 Binary files /dev/null and b/tts/cache/0f831c23158cf948b74f2af5c4da82d7.wav differ diff --git a/tts/cache/0f95ac63f816fa3692b31498648b2953.wav b/tts/cache/0f95ac63f816fa3692b31498648b2953.wav new file mode 100644 index 0000000..822fdff Binary files /dev/null and b/tts/cache/0f95ac63f816fa3692b31498648b2953.wav differ diff --git a/tts/cache/0fa7a10401e33e94de2e2c84f41d2656.wav b/tts/cache/0fa7a10401e33e94de2e2c84f41d2656.wav new file mode 100644 index 0000000..7e17600 Binary files /dev/null and b/tts/cache/0fa7a10401e33e94de2e2c84f41d2656.wav differ diff --git a/tts/cache/0fd42028f41eb3cb67f1b56d232e3de6.wav b/tts/cache/0fd42028f41eb3cb67f1b56d232e3de6.wav new file mode 100644 index 0000000..6c7e7a9 Binary files /dev/null and b/tts/cache/0fd42028f41eb3cb67f1b56d232e3de6.wav differ diff --git a/tts/cache/1237668f9973ed508d18e8fe6ba59a57.wav b/tts/cache/1237668f9973ed508d18e8fe6ba59a57.wav new file mode 100644 index 0000000..a20c6f4 Binary files /dev/null and b/tts/cache/1237668f9973ed508d18e8fe6ba59a57.wav differ diff --git a/tts/cache/13239073213b0471ddcd4f6fa71a0484.wav b/tts/cache/13239073213b0471ddcd4f6fa71a0484.wav new file mode 100644 index 0000000..00abaf8 Binary files /dev/null and b/tts/cache/13239073213b0471ddcd4f6fa71a0484.wav differ diff --git a/tts/cache/140d602c3941489184eb88df1ed25f52.wav b/tts/cache/140d602c3941489184eb88df1ed25f52.wav new file mode 100644 index 0000000..048aced Binary files /dev/null and b/tts/cache/140d602c3941489184eb88df1ed25f52.wav differ diff --git a/tts/cache/14248da949d131d6e480d88ed76e1f77.wav b/tts/cache/14248da949d131d6e480d88ed76e1f77.wav new file mode 100644 index 0000000..920bae5 Binary files /dev/null and b/tts/cache/14248da949d131d6e480d88ed76e1f77.wav differ diff --git a/tts/cache/144b02dfaa87c3bcd88b71a10d67454f.wav b/tts/cache/144b02dfaa87c3bcd88b71a10d67454f.wav new file mode 100644 index 0000000..100fb77 Binary files /dev/null and b/tts/cache/144b02dfaa87c3bcd88b71a10d67454f.wav differ diff --git a/tts/cache/14c2f39baa7ec8a165676df9b9c1617c.wav b/tts/cache/14c2f39baa7ec8a165676df9b9c1617c.wav new file mode 100644 index 0000000..044dce6 Binary files /dev/null and b/tts/cache/14c2f39baa7ec8a165676df9b9c1617c.wav differ diff --git a/tts/cache/14f3a921869d7347bf5172eb03be5ba9.wav b/tts/cache/14f3a921869d7347bf5172eb03be5ba9.wav new file mode 100644 index 0000000..e268ad3 Binary files /dev/null and b/tts/cache/14f3a921869d7347bf5172eb03be5ba9.wav differ diff --git a/tts/cache/15e08fb6b6875c5c36c678ef526d27e7.wav b/tts/cache/15e08fb6b6875c5c36c678ef526d27e7.wav new file mode 100644 index 0000000..7f39cbd Binary files /dev/null and b/tts/cache/15e08fb6b6875c5c36c678ef526d27e7.wav differ diff --git a/tts/cache/15eecbf2c881e3559822226cc8dc147a.wav b/tts/cache/15eecbf2c881e3559822226cc8dc147a.wav new file mode 100644 index 0000000..038294f Binary files /dev/null and b/tts/cache/15eecbf2c881e3559822226cc8dc147a.wav differ diff --git a/tts/cache/163893985a76fa9127f516b1e0659318.wav b/tts/cache/163893985a76fa9127f516b1e0659318.wav new file mode 100644 index 0000000..2f14807 Binary files /dev/null and b/tts/cache/163893985a76fa9127f516b1e0659318.wav differ diff --git a/tts/cache/163c47eba0c40414a872e7d5cee4de18.wav b/tts/cache/163c47eba0c40414a872e7d5cee4de18.wav new file mode 100644 index 0000000..30849b0 Binary files /dev/null and b/tts/cache/163c47eba0c40414a872e7d5cee4de18.wav differ diff --git a/tts/cache/165889268e3770842a64948dda27e5c7.wav b/tts/cache/165889268e3770842a64948dda27e5c7.wav new file mode 100644 index 0000000..5116326 Binary files /dev/null and b/tts/cache/165889268e3770842a64948dda27e5c7.wav differ diff --git a/tts/cache/16dd6ddf433ae63ec6867db623a398df.wav b/tts/cache/16dd6ddf433ae63ec6867db623a398df.wav new file mode 100644 index 0000000..484e029 Binary files /dev/null and b/tts/cache/16dd6ddf433ae63ec6867db623a398df.wav differ diff --git a/tts/cache/17ac63f3fae0627e063252ef38fb0cff.wav b/tts/cache/17ac63f3fae0627e063252ef38fb0cff.wav new file mode 100644 index 0000000..778287b Binary files /dev/null and b/tts/cache/17ac63f3fae0627e063252ef38fb0cff.wav differ diff --git a/tts/cache/18050c43ce633195fa0e5f5b40118a03.wav b/tts/cache/18050c43ce633195fa0e5f5b40118a03.wav new file mode 100644 index 0000000..9e71186 Binary files /dev/null and b/tts/cache/18050c43ce633195fa0e5f5b40118a03.wav differ diff --git a/tts/cache/1841d61da90e5854bf6de5f614cc2795.wav b/tts/cache/1841d61da90e5854bf6de5f614cc2795.wav new file mode 100644 index 0000000..7562537 Binary files /dev/null and b/tts/cache/1841d61da90e5854bf6de5f614cc2795.wav differ diff --git a/tts/cache/18c064d4a0d0252ab069c508027bce64.wav b/tts/cache/18c064d4a0d0252ab069c508027bce64.wav new file mode 100644 index 0000000..fb0cfe9 Binary files /dev/null and b/tts/cache/18c064d4a0d0252ab069c508027bce64.wav differ diff --git a/tts/cache/18f28ed559617caeab46cd32d6638849.wav b/tts/cache/18f28ed559617caeab46cd32d6638849.wav new file mode 100644 index 0000000..65dc23c Binary files /dev/null and b/tts/cache/18f28ed559617caeab46cd32d6638849.wav differ diff --git a/tts/cache/19e30466ff81e4a94e03c491568b893b.wav b/tts/cache/19e30466ff81e4a94e03c491568b893b.wav new file mode 100644 index 0000000..781664e Binary files /dev/null and b/tts/cache/19e30466ff81e4a94e03c491568b893b.wav differ diff --git a/tts/cache/1a5e186133e4ab0f5bf3a9183e0cda32.wav b/tts/cache/1a5e186133e4ab0f5bf3a9183e0cda32.wav new file mode 100644 index 0000000..e5058ce Binary files /dev/null and b/tts/cache/1a5e186133e4ab0f5bf3a9183e0cda32.wav differ diff --git a/tts/cache/1a7894a45cacff017dc3aa0f03bb770a.wav b/tts/cache/1a7894a45cacff017dc3aa0f03bb770a.wav new file mode 100644 index 0000000..16a204e Binary files /dev/null and b/tts/cache/1a7894a45cacff017dc3aa0f03bb770a.wav differ diff --git a/tts/cache/1a91b5104eeb719c1e75d0a88576d7c0.wav b/tts/cache/1a91b5104eeb719c1e75d0a88576d7c0.wav new file mode 100644 index 0000000..5fce89c Binary files /dev/null and b/tts/cache/1a91b5104eeb719c1e75d0a88576d7c0.wav differ diff --git a/tts/cache/1b66a3c0b2c4f8f857dac6cab9c2ff8f.wav b/tts/cache/1b66a3c0b2c4f8f857dac6cab9c2ff8f.wav new file mode 100644 index 0000000..305d642 Binary files /dev/null and b/tts/cache/1b66a3c0b2c4f8f857dac6cab9c2ff8f.wav differ diff --git a/tts/cache/1b85b3fd1facfc7d1b6c5a7510469781.wav b/tts/cache/1b85b3fd1facfc7d1b6c5a7510469781.wav new file mode 100644 index 0000000..239a2b2 Binary files /dev/null and b/tts/cache/1b85b3fd1facfc7d1b6c5a7510469781.wav differ diff --git a/tts/cache/1bba21cf79c2965e12ae01dfb86dad95.wav b/tts/cache/1bba21cf79c2965e12ae01dfb86dad95.wav new file mode 100644 index 0000000..4653f66 Binary files /dev/null and b/tts/cache/1bba21cf79c2965e12ae01dfb86dad95.wav differ diff --git a/tts/cache/1bc20d4b254e7b7edcdda22bb47c38b7.wav b/tts/cache/1bc20d4b254e7b7edcdda22bb47c38b7.wav new file mode 100644 index 0000000..5bdb842 Binary files /dev/null and b/tts/cache/1bc20d4b254e7b7edcdda22bb47c38b7.wav differ diff --git a/tts/cache/1c8ce4a0b8d7e28e0ef8841c12904488.wav b/tts/cache/1c8ce4a0b8d7e28e0ef8841c12904488.wav new file mode 100644 index 0000000..e01b5f9 Binary files /dev/null and b/tts/cache/1c8ce4a0b8d7e28e0ef8841c12904488.wav differ diff --git a/tts/cache/1dac2e291a315200aa6f9970500d05d8.wav b/tts/cache/1dac2e291a315200aa6f9970500d05d8.wav new file mode 100644 index 0000000..480ad59 Binary files /dev/null and b/tts/cache/1dac2e291a315200aa6f9970500d05d8.wav differ diff --git a/tts/cache/1dfcc65ef3a36af51c978db9a8e3c767.wav b/tts/cache/1dfcc65ef3a36af51c978db9a8e3c767.wav new file mode 100644 index 0000000..651c1ef Binary files /dev/null and b/tts/cache/1dfcc65ef3a36af51c978db9a8e3c767.wav differ diff --git a/tts/cache/1ea8ac74f6fe13b0087bd16b190a25dd.wav b/tts/cache/1ea8ac74f6fe13b0087bd16b190a25dd.wav new file mode 100644 index 0000000..d0617e0 Binary files /dev/null and b/tts/cache/1ea8ac74f6fe13b0087bd16b190a25dd.wav differ diff --git a/tts/cache/1f77a47f2e666b27ccfe1e564ecdf446.wav b/tts/cache/1f77a47f2e666b27ccfe1e564ecdf446.wav new file mode 100644 index 0000000..7761e79 Binary files /dev/null and b/tts/cache/1f77a47f2e666b27ccfe1e564ecdf446.wav differ diff --git a/tts/cache/207e16ba5bfdaa5361d9b95323e86add.wav b/tts/cache/207e16ba5bfdaa5361d9b95323e86add.wav new file mode 100644 index 0000000..e130bf1 Binary files /dev/null and b/tts/cache/207e16ba5bfdaa5361d9b95323e86add.wav differ diff --git a/tts/cache/2111311fa30d3f0f1c794fe144cc8767.wav b/tts/cache/2111311fa30d3f0f1c794fe144cc8767.wav new file mode 100644 index 0000000..8292bba Binary files /dev/null and b/tts/cache/2111311fa30d3f0f1c794fe144cc8767.wav differ diff --git a/tts/cache/2111d4fe1d591ff243509699a8a334a4.wav b/tts/cache/2111d4fe1d591ff243509699a8a334a4.wav new file mode 100644 index 0000000..abe6d4e Binary files /dev/null and b/tts/cache/2111d4fe1d591ff243509699a8a334a4.wav differ diff --git a/tts/cache/217f195fc82d25cc131f1b7ed6dd3ec9.wav b/tts/cache/217f195fc82d25cc131f1b7ed6dd3ec9.wav new file mode 100644 index 0000000..4048e6c Binary files /dev/null and b/tts/cache/217f195fc82d25cc131f1b7ed6dd3ec9.wav differ diff --git a/tts/cache/221b9d7fb5d5c16fb0fac5bd790a8851.wav b/tts/cache/221b9d7fb5d5c16fb0fac5bd790a8851.wav new file mode 100644 index 0000000..95d92f2 Binary files /dev/null and b/tts/cache/221b9d7fb5d5c16fb0fac5bd790a8851.wav differ diff --git a/tts/cache/2270a205cd85063bd737f4898e016263.wav b/tts/cache/2270a205cd85063bd737f4898e016263.wav new file mode 100644 index 0000000..c032540 Binary files /dev/null and b/tts/cache/2270a205cd85063bd737f4898e016263.wav differ diff --git a/tts/cache/22e671e9625e8c39c08eeec893cbbbaf.wav b/tts/cache/22e671e9625e8c39c08eeec893cbbbaf.wav new file mode 100644 index 0000000..0fcae4c Binary files /dev/null and b/tts/cache/22e671e9625e8c39c08eeec893cbbbaf.wav differ diff --git a/tts/cache/22eece102a71245559ed335c77e978d4.wav b/tts/cache/22eece102a71245559ed335c77e978d4.wav new file mode 100644 index 0000000..08eaee1 Binary files /dev/null and b/tts/cache/22eece102a71245559ed335c77e978d4.wav differ diff --git a/tts/cache/2332d828e1a988ca3275f10799fb9281.wav b/tts/cache/2332d828e1a988ca3275f10799fb9281.wav new file mode 100644 index 0000000..9ab69b7 Binary files /dev/null and b/tts/cache/2332d828e1a988ca3275f10799fb9281.wav differ diff --git a/tts/cache/235506a81dc4e45b5491ae83d448c0c9.wav b/tts/cache/235506a81dc4e45b5491ae83d448c0c9.wav new file mode 100644 index 0000000..29fba95 Binary files /dev/null and b/tts/cache/235506a81dc4e45b5491ae83d448c0c9.wav differ diff --git a/tts/cache/2356c03d15f4471558f40cd7ac0806b5.wav b/tts/cache/2356c03d15f4471558f40cd7ac0806b5.wav new file mode 100644 index 0000000..53fb0b2 Binary files /dev/null and b/tts/cache/2356c03d15f4471558f40cd7ac0806b5.wav differ diff --git a/tts/cache/252c5899f7803a492c4f566bd11adbfd.wav b/tts/cache/252c5899f7803a492c4f566bd11adbfd.wav new file mode 100644 index 0000000..acacd7f Binary files /dev/null and b/tts/cache/252c5899f7803a492c4f566bd11adbfd.wav differ diff --git a/tts/cache/256169edaf281d2ed8645de0ec438a54.wav b/tts/cache/256169edaf281d2ed8645de0ec438a54.wav new file mode 100644 index 0000000..16a830e Binary files /dev/null and b/tts/cache/256169edaf281d2ed8645de0ec438a54.wav differ diff --git a/tts/cache/25ae2ab2ef0b3d3bdb69c24e603a0c5b.wav b/tts/cache/25ae2ab2ef0b3d3bdb69c24e603a0c5b.wav new file mode 100644 index 0000000..15a918c Binary files /dev/null and b/tts/cache/25ae2ab2ef0b3d3bdb69c24e603a0c5b.wav differ diff --git a/tts/cache/26550d2b5801a546cab9af07cf8197c5.wav b/tts/cache/26550d2b5801a546cab9af07cf8197c5.wav new file mode 100644 index 0000000..74ca213 Binary files /dev/null and b/tts/cache/26550d2b5801a546cab9af07cf8197c5.wav differ diff --git a/tts/cache/265b47dae17de13695d9164810d181a4.wav b/tts/cache/265b47dae17de13695d9164810d181a4.wav new file mode 100644 index 0000000..4132455 Binary files /dev/null and b/tts/cache/265b47dae17de13695d9164810d181a4.wav differ diff --git a/tts/cache/274d8ab2f09d327c473946bab0135388.wav b/tts/cache/274d8ab2f09d327c473946bab0135388.wav new file mode 100644 index 0000000..a015f2f Binary files /dev/null and b/tts/cache/274d8ab2f09d327c473946bab0135388.wav differ diff --git a/tts/cache/28941bfb05111fa8a56ea77afa1272c9.wav b/tts/cache/28941bfb05111fa8a56ea77afa1272c9.wav new file mode 100644 index 0000000..cbbdf4c Binary files /dev/null and b/tts/cache/28941bfb05111fa8a56ea77afa1272c9.wav differ diff --git a/tts/cache/28abe36999f16357d157993fe019096e.wav b/tts/cache/28abe36999f16357d157993fe019096e.wav new file mode 100644 index 0000000..bc465e5 Binary files /dev/null and b/tts/cache/28abe36999f16357d157993fe019096e.wav differ diff --git a/tts/cache/28f1b9230add1a378289ea189f6da384.wav b/tts/cache/28f1b9230add1a378289ea189f6da384.wav new file mode 100644 index 0000000..a7ac40c Binary files /dev/null and b/tts/cache/28f1b9230add1a378289ea189f6da384.wav differ diff --git a/tts/cache/2aa78abc45554044d7f964ba9c8b0139.wav b/tts/cache/2aa78abc45554044d7f964ba9c8b0139.wav new file mode 100644 index 0000000..aa72302 Binary files /dev/null and b/tts/cache/2aa78abc45554044d7f964ba9c8b0139.wav differ diff --git a/tts/cache/2b2796df536e031bf44600ebd65b963d.wav b/tts/cache/2b2796df536e031bf44600ebd65b963d.wav new file mode 100644 index 0000000..317bd7c Binary files /dev/null and b/tts/cache/2b2796df536e031bf44600ebd65b963d.wav differ diff --git a/tts/cache/2ba9be42f7daa54d29c29e355b1bfbef.wav b/tts/cache/2ba9be42f7daa54d29c29e355b1bfbef.wav new file mode 100644 index 0000000..fe368dc Binary files /dev/null and b/tts/cache/2ba9be42f7daa54d29c29e355b1bfbef.wav differ diff --git a/tts/cache/2c2eb3147fc3d078079e077259575b24.wav b/tts/cache/2c2eb3147fc3d078079e077259575b24.wav new file mode 100644 index 0000000..ef788c5 Binary files /dev/null and b/tts/cache/2c2eb3147fc3d078079e077259575b24.wav differ diff --git a/tts/cache/2c3d2a89e23657890acef75113c2e8bf.wav b/tts/cache/2c3d2a89e23657890acef75113c2e8bf.wav new file mode 100644 index 0000000..bb07e6a Binary files /dev/null and b/tts/cache/2c3d2a89e23657890acef75113c2e8bf.wav differ diff --git a/tts/cache/2c9e2972b2d87254368748bcfdce973f.wav b/tts/cache/2c9e2972b2d87254368748bcfdce973f.wav new file mode 100644 index 0000000..2c7e9c4 Binary files /dev/null and b/tts/cache/2c9e2972b2d87254368748bcfdce973f.wav differ diff --git a/tts/cache/2d6d23eb7c5ff2915942ebf25d1580b8.wav b/tts/cache/2d6d23eb7c5ff2915942ebf25d1580b8.wav new file mode 100644 index 0000000..8b9bafc Binary files /dev/null and b/tts/cache/2d6d23eb7c5ff2915942ebf25d1580b8.wav differ diff --git a/tts/cache/2dcfc4c53bc54f6ff6fb55ffb5ccbd3f.wav b/tts/cache/2dcfc4c53bc54f6ff6fb55ffb5ccbd3f.wav new file mode 100644 index 0000000..5085fdb Binary files /dev/null and b/tts/cache/2dcfc4c53bc54f6ff6fb55ffb5ccbd3f.wav differ diff --git a/tts/cache/2dfa244884725e6f3a06ebd1b98bcf76.wav b/tts/cache/2dfa244884725e6f3a06ebd1b98bcf76.wav new file mode 100644 index 0000000..bc09703 Binary files /dev/null and b/tts/cache/2dfa244884725e6f3a06ebd1b98bcf76.wav differ diff --git a/tts/cache/2dfc6caa15b416aa6169b77f8478734e.wav b/tts/cache/2dfc6caa15b416aa6169b77f8478734e.wav new file mode 100644 index 0000000..c9cd8e0 Binary files /dev/null and b/tts/cache/2dfc6caa15b416aa6169b77f8478734e.wav differ diff --git a/tts/cache/2e734366d12ebaca4db152c4ff6e0c26.wav b/tts/cache/2e734366d12ebaca4db152c4ff6e0c26.wav new file mode 100644 index 0000000..9e7f2fd Binary files /dev/null and b/tts/cache/2e734366d12ebaca4db152c4ff6e0c26.wav differ diff --git a/tts/cache/2f1fba08ecf90090b4282a0894d353c0.wav b/tts/cache/2f1fba08ecf90090b4282a0894d353c0.wav new file mode 100644 index 0000000..a2fccf1 Binary files /dev/null and b/tts/cache/2f1fba08ecf90090b4282a0894d353c0.wav differ diff --git a/tts/cache/2f3fd5ba815f8905f68c3a25c5a1bb7a.wav b/tts/cache/2f3fd5ba815f8905f68c3a25c5a1bb7a.wav new file mode 100644 index 0000000..de1fa89 Binary files /dev/null and b/tts/cache/2f3fd5ba815f8905f68c3a25c5a1bb7a.wav differ diff --git a/tts/cache/33c13dd6fe08e20aaf8f5ef387a8dbff.wav b/tts/cache/33c13dd6fe08e20aaf8f5ef387a8dbff.wav new file mode 100644 index 0000000..18f14f3 Binary files /dev/null and b/tts/cache/33c13dd6fe08e20aaf8f5ef387a8dbff.wav differ diff --git a/tts/cache/34813b285b6c6810319b7fd3fa758541.wav b/tts/cache/34813b285b6c6810319b7fd3fa758541.wav new file mode 100644 index 0000000..e152ac7 Binary files /dev/null and b/tts/cache/34813b285b6c6810319b7fd3fa758541.wav differ diff --git a/tts/cache/3484b7665b09985b0dd701a89db52dfc.wav b/tts/cache/3484b7665b09985b0dd701a89db52dfc.wav new file mode 100644 index 0000000..163bd27 Binary files /dev/null and b/tts/cache/3484b7665b09985b0dd701a89db52dfc.wav differ diff --git a/tts/cache/34ad20b2098c1a4b2bbdd625c1549014.wav b/tts/cache/34ad20b2098c1a4b2bbdd625c1549014.wav new file mode 100644 index 0000000..f46493f Binary files /dev/null and b/tts/cache/34ad20b2098c1a4b2bbdd625c1549014.wav differ diff --git a/tts/cache/3501a7dce1b179917543763be45f392c.wav b/tts/cache/3501a7dce1b179917543763be45f392c.wav new file mode 100644 index 0000000..cf72916 Binary files /dev/null and b/tts/cache/3501a7dce1b179917543763be45f392c.wav differ diff --git a/tts/cache/35f4568376a01bec7289cc6fb5036b3e.wav b/tts/cache/35f4568376a01bec7289cc6fb5036b3e.wav new file mode 100644 index 0000000..8a41436 Binary files /dev/null and b/tts/cache/35f4568376a01bec7289cc6fb5036b3e.wav differ diff --git a/tts/cache/3604f8937eadf07e28645fddf451eeaa.wav b/tts/cache/3604f8937eadf07e28645fddf451eeaa.wav new file mode 100644 index 0000000..e64b2b3 Binary files /dev/null and b/tts/cache/3604f8937eadf07e28645fddf451eeaa.wav differ diff --git a/tts/cache/36635af49b887a377cb02194fb709232.wav b/tts/cache/36635af49b887a377cb02194fb709232.wav new file mode 100644 index 0000000..2a6bd3c Binary files /dev/null and b/tts/cache/36635af49b887a377cb02194fb709232.wav differ diff --git a/tts/cache/3682b4ddaf8aabbc3887313be0331d91.wav b/tts/cache/3682b4ddaf8aabbc3887313be0331d91.wav new file mode 100644 index 0000000..42cc830 Binary files /dev/null and b/tts/cache/3682b4ddaf8aabbc3887313be0331d91.wav differ diff --git a/tts/cache/36c90492def20b8509000500fcdc8a5b.wav b/tts/cache/36c90492def20b8509000500fcdc8a5b.wav new file mode 100644 index 0000000..6e07d14 Binary files /dev/null and b/tts/cache/36c90492def20b8509000500fcdc8a5b.wav differ diff --git a/tts/cache/37343236a3c2496d7cb042184b041941.wav b/tts/cache/37343236a3c2496d7cb042184b041941.wav new file mode 100644 index 0000000..cd15113 Binary files /dev/null and b/tts/cache/37343236a3c2496d7cb042184b041941.wav differ diff --git a/tts/cache/3773037d2089ffb7be468d08f8809d27.wav b/tts/cache/3773037d2089ffb7be468d08f8809d27.wav new file mode 100644 index 0000000..817a2e4 Binary files /dev/null and b/tts/cache/3773037d2089ffb7be468d08f8809d27.wav differ diff --git a/tts/cache/37af7bee94d167696723849760f2ce8f.wav b/tts/cache/37af7bee94d167696723849760f2ce8f.wav new file mode 100644 index 0000000..19022e3 Binary files /dev/null and b/tts/cache/37af7bee94d167696723849760f2ce8f.wav differ diff --git a/tts/cache/37e824efa57953cd3ceddaceb9a34507.wav b/tts/cache/37e824efa57953cd3ceddaceb9a34507.wav new file mode 100644 index 0000000..95a67ff Binary files /dev/null and b/tts/cache/37e824efa57953cd3ceddaceb9a34507.wav differ diff --git a/tts/cache/386f7d5b1a6a797e0eccdd5bfa068897.wav b/tts/cache/386f7d5b1a6a797e0eccdd5bfa068897.wav new file mode 100644 index 0000000..310ad7e Binary files /dev/null and b/tts/cache/386f7d5b1a6a797e0eccdd5bfa068897.wav differ diff --git a/tts/cache/399167fa3de3453dd183fb9f7759baf2.wav b/tts/cache/399167fa3de3453dd183fb9f7759baf2.wav new file mode 100644 index 0000000..88c1ef4 Binary files /dev/null and b/tts/cache/399167fa3de3453dd183fb9f7759baf2.wav differ diff --git a/tts/cache/3aab9bee26ea9a0f9442b56a4345c782.wav b/tts/cache/3aab9bee26ea9a0f9442b56a4345c782.wav new file mode 100644 index 0000000..747a8fe Binary files /dev/null and b/tts/cache/3aab9bee26ea9a0f9442b56a4345c782.wav differ diff --git a/tts/cache/3ad73f812aa1cc49145a08665035c0d6.wav b/tts/cache/3ad73f812aa1cc49145a08665035c0d6.wav new file mode 100644 index 0000000..858f9ab Binary files /dev/null and b/tts/cache/3ad73f812aa1cc49145a08665035c0d6.wav differ diff --git a/tts/cache/3b061c99250476cf7d028216015346b3.wav b/tts/cache/3b061c99250476cf7d028216015346b3.wav new file mode 100644 index 0000000..c8c3e7a Binary files /dev/null and b/tts/cache/3b061c99250476cf7d028216015346b3.wav differ diff --git a/tts/cache/3bb87647b36e3d1baf9884a990e8bdf8.wav b/tts/cache/3bb87647b36e3d1baf9884a990e8bdf8.wav new file mode 100644 index 0000000..32dcb20 Binary files /dev/null and b/tts/cache/3bb87647b36e3d1baf9884a990e8bdf8.wav differ diff --git a/tts/cache/3c913aa4eba84bc2996d9b77de30841a.wav b/tts/cache/3c913aa4eba84bc2996d9b77de30841a.wav new file mode 100644 index 0000000..b7c0786 Binary files /dev/null and b/tts/cache/3c913aa4eba84bc2996d9b77de30841a.wav differ diff --git a/tts/cache/3d4ecca6cc4debcc0b9b451bf586603a.wav b/tts/cache/3d4ecca6cc4debcc0b9b451bf586603a.wav new file mode 100644 index 0000000..e8b058f Binary files /dev/null and b/tts/cache/3d4ecca6cc4debcc0b9b451bf586603a.wav differ diff --git a/tts/cache/3ffaeea72c35287011a1e52b72105f10.wav b/tts/cache/3ffaeea72c35287011a1e52b72105f10.wav new file mode 100644 index 0000000..4cce38c Binary files /dev/null and b/tts/cache/3ffaeea72c35287011a1e52b72105f10.wav differ diff --git a/tts/cache/41095e506e5505f39ef0957e9a1b0c25.wav b/tts/cache/41095e506e5505f39ef0957e9a1b0c25.wav new file mode 100644 index 0000000..bf93da1 Binary files /dev/null and b/tts/cache/41095e506e5505f39ef0957e9a1b0c25.wav differ diff --git a/tts/cache/4153b38f34047aaa80e3b1ad70dc33b3.wav b/tts/cache/4153b38f34047aaa80e3b1ad70dc33b3.wav new file mode 100644 index 0000000..51e2ad0 Binary files /dev/null and b/tts/cache/4153b38f34047aaa80e3b1ad70dc33b3.wav differ diff --git a/tts/cache/41649fe94219c6b9c504eb871a48cabe.wav b/tts/cache/41649fe94219c6b9c504eb871a48cabe.wav new file mode 100644 index 0000000..3845a64 Binary files /dev/null and b/tts/cache/41649fe94219c6b9c504eb871a48cabe.wav differ diff --git a/tts/cache/4352d50833349687f9789fa6e32fec0c.wav b/tts/cache/4352d50833349687f9789fa6e32fec0c.wav new file mode 100644 index 0000000..0f5fea7 Binary files /dev/null and b/tts/cache/4352d50833349687f9789fa6e32fec0c.wav differ diff --git a/tts/cache/44249ead4dafb211dddb2ef742e31dda.wav b/tts/cache/44249ead4dafb211dddb2ef742e31dda.wav new file mode 100644 index 0000000..7ed11c4 Binary files /dev/null and b/tts/cache/44249ead4dafb211dddb2ef742e31dda.wav differ diff --git a/tts/cache/442673f652c5361657a357d60e4f3408.wav b/tts/cache/442673f652c5361657a357d60e4f3408.wav new file mode 100644 index 0000000..7d4e62e Binary files /dev/null and b/tts/cache/442673f652c5361657a357d60e4f3408.wav differ diff --git a/tts/cache/4450af38e2074d8c1cb58c93636a9f47.wav b/tts/cache/4450af38e2074d8c1cb58c93636a9f47.wav new file mode 100644 index 0000000..2c43868 Binary files /dev/null and b/tts/cache/4450af38e2074d8c1cb58c93636a9f47.wav differ diff --git a/tts/cache/44bbe3a6177690e1fb00ab98e5e41ff4.wav b/tts/cache/44bbe3a6177690e1fb00ab98e5e41ff4.wav new file mode 100644 index 0000000..a01ae12 Binary files /dev/null and b/tts/cache/44bbe3a6177690e1fb00ab98e5e41ff4.wav differ diff --git a/tts/cache/44ebd7989f7ad3b4d1ec653c39ef8596.wav b/tts/cache/44ebd7989f7ad3b4d1ec653c39ef8596.wav new file mode 100644 index 0000000..ee62b6c Binary files /dev/null and b/tts/cache/44ebd7989f7ad3b4d1ec653c39ef8596.wav differ diff --git a/tts/cache/45477ca0161314534fdc3e6bee8ea8fc.wav b/tts/cache/45477ca0161314534fdc3e6bee8ea8fc.wav new file mode 100644 index 0000000..00108bb Binary files /dev/null and b/tts/cache/45477ca0161314534fdc3e6bee8ea8fc.wav differ diff --git a/tts/cache/45b94b709bbdd595beafa7e199a97e3e.wav b/tts/cache/45b94b709bbdd595beafa7e199a97e3e.wav new file mode 100644 index 0000000..d2f1fcb Binary files /dev/null and b/tts/cache/45b94b709bbdd595beafa7e199a97e3e.wav differ diff --git a/tts/cache/466dac7cfd7c58eb605fbbf161086077.wav b/tts/cache/466dac7cfd7c58eb605fbbf161086077.wav new file mode 100644 index 0000000..f77b4b0 Binary files /dev/null and b/tts/cache/466dac7cfd7c58eb605fbbf161086077.wav differ diff --git a/tts/cache/467f00d51d2f5e11dae89fab975fc36d.wav b/tts/cache/467f00d51d2f5e11dae89fab975fc36d.wav new file mode 100644 index 0000000..9e72d01 Binary files /dev/null and b/tts/cache/467f00d51d2f5e11dae89fab975fc36d.wav differ diff --git a/tts/cache/469865a09dd6734f96bd8c4a33ecc9d1.wav b/tts/cache/469865a09dd6734f96bd8c4a33ecc9d1.wav new file mode 100644 index 0000000..6d445dd Binary files /dev/null and b/tts/cache/469865a09dd6734f96bd8c4a33ecc9d1.wav differ diff --git a/tts/cache/46a8ccb5eeedd9b4463cb59c22afdc82.wav b/tts/cache/46a8ccb5eeedd9b4463cb59c22afdc82.wav new file mode 100644 index 0000000..bba1433 Binary files /dev/null and b/tts/cache/46a8ccb5eeedd9b4463cb59c22afdc82.wav differ diff --git a/tts/cache/4726e5754efe85021aa47db880baa8ce.wav b/tts/cache/4726e5754efe85021aa47db880baa8ce.wav new file mode 100644 index 0000000..9789381 Binary files /dev/null and b/tts/cache/4726e5754efe85021aa47db880baa8ce.wav differ diff --git a/tts/cache/4770f88e6b65c1462eae4edb29ebc9f5.wav b/tts/cache/4770f88e6b65c1462eae4edb29ebc9f5.wav new file mode 100644 index 0000000..78b1b88 Binary files /dev/null and b/tts/cache/4770f88e6b65c1462eae4edb29ebc9f5.wav differ diff --git a/tts/cache/47ab0d312b467841b447d50df33d5889.wav b/tts/cache/47ab0d312b467841b447d50df33d5889.wav new file mode 100644 index 0000000..d1e65ed Binary files /dev/null and b/tts/cache/47ab0d312b467841b447d50df33d5889.wav differ diff --git a/tts/cache/4856eb1dde688bd45456c8ab386e323e.wav b/tts/cache/4856eb1dde688bd45456c8ab386e323e.wav new file mode 100644 index 0000000..8eefbe3 Binary files /dev/null and b/tts/cache/4856eb1dde688bd45456c8ab386e323e.wav differ diff --git a/tts/cache/4931d7b12856da0270b9a505ca40fa79.wav b/tts/cache/4931d7b12856da0270b9a505ca40fa79.wav new file mode 100644 index 0000000..673ed17 Binary files /dev/null and b/tts/cache/4931d7b12856da0270b9a505ca40fa79.wav differ diff --git a/tts/cache/494a85df78f5b4075a114d61d11d78f5.wav b/tts/cache/494a85df78f5b4075a114d61d11d78f5.wav new file mode 100644 index 0000000..472d52b Binary files /dev/null and b/tts/cache/494a85df78f5b4075a114d61d11d78f5.wav differ diff --git a/tts/cache/49c447b9d181a1815a3375064f3c5e97.wav b/tts/cache/49c447b9d181a1815a3375064f3c5e97.wav new file mode 100644 index 0000000..412d0d6 Binary files /dev/null and b/tts/cache/49c447b9d181a1815a3375064f3c5e97.wav differ diff --git a/tts/cache/4a0bfc095d75d194e7648eb5a4472cbf.wav b/tts/cache/4a0bfc095d75d194e7648eb5a4472cbf.wav new file mode 100644 index 0000000..cfcadf6 Binary files /dev/null and b/tts/cache/4a0bfc095d75d194e7648eb5a4472cbf.wav differ diff --git a/tts/cache/4a0f44b4702e26707684a7d3535ba406.wav b/tts/cache/4a0f44b4702e26707684a7d3535ba406.wav new file mode 100644 index 0000000..0f85118 Binary files /dev/null and b/tts/cache/4a0f44b4702e26707684a7d3535ba406.wav differ diff --git a/tts/cache/4b0e5c9deed28df3611eca9f7419e967.wav b/tts/cache/4b0e5c9deed28df3611eca9f7419e967.wav new file mode 100644 index 0000000..986982f Binary files /dev/null and b/tts/cache/4b0e5c9deed28df3611eca9f7419e967.wav differ diff --git a/tts/cache/4bec1040847bc43f1c02d09045ea2c73.wav b/tts/cache/4bec1040847bc43f1c02d09045ea2c73.wav new file mode 100644 index 0000000..4367b33 Binary files /dev/null and b/tts/cache/4bec1040847bc43f1c02d09045ea2c73.wav differ diff --git a/tts/cache/4c0afd85b4b4b225cc360fbb5abffd01.wav b/tts/cache/4c0afd85b4b4b225cc360fbb5abffd01.wav new file mode 100644 index 0000000..373debd Binary files /dev/null and b/tts/cache/4c0afd85b4b4b225cc360fbb5abffd01.wav differ diff --git a/tts/cache/4c5fc79842b7573eff5cabb035548947.wav b/tts/cache/4c5fc79842b7573eff5cabb035548947.wav new file mode 100644 index 0000000..857a3f5 Binary files /dev/null and b/tts/cache/4c5fc79842b7573eff5cabb035548947.wav differ diff --git a/tts/cache/4c797f149471fa4fea4e3435db38a498.wav b/tts/cache/4c797f149471fa4fea4e3435db38a498.wav new file mode 100644 index 0000000..838ad3a Binary files /dev/null and b/tts/cache/4c797f149471fa4fea4e3435db38a498.wav differ diff --git a/tts/cache/4daabd7d27ba8a4cc9e671f89c412ddd.wav b/tts/cache/4daabd7d27ba8a4cc9e671f89c412ddd.wav new file mode 100644 index 0000000..716c7b3 Binary files /dev/null and b/tts/cache/4daabd7d27ba8a4cc9e671f89c412ddd.wav differ diff --git a/tts/cache/4f6d1961bf3ed9a5fb26c81d497c3180.wav b/tts/cache/4f6d1961bf3ed9a5fb26c81d497c3180.wav new file mode 100644 index 0000000..261ea7d Binary files /dev/null and b/tts/cache/4f6d1961bf3ed9a5fb26c81d497c3180.wav differ diff --git a/tts/cache/4f95d9f7322709f643c5175c132295fd.wav b/tts/cache/4f95d9f7322709f643c5175c132295fd.wav new file mode 100644 index 0000000..3e7139c Binary files /dev/null and b/tts/cache/4f95d9f7322709f643c5175c132295fd.wav differ diff --git a/tts/cache/503d2d36ba2f7484aca90f5f9d6ed14f.wav b/tts/cache/503d2d36ba2f7484aca90f5f9d6ed14f.wav new file mode 100644 index 0000000..e58b859 Binary files /dev/null and b/tts/cache/503d2d36ba2f7484aca90f5f9d6ed14f.wav differ diff --git a/tts/cache/51454b450c71c4d95d9cd5730a00f130.wav b/tts/cache/51454b450c71c4d95d9cd5730a00f130.wav new file mode 100644 index 0000000..e3b85bf Binary files /dev/null and b/tts/cache/51454b450c71c4d95d9cd5730a00f130.wav differ diff --git a/tts/cache/51883c9652913092deb2a188040eac52.wav b/tts/cache/51883c9652913092deb2a188040eac52.wav new file mode 100644 index 0000000..25e5c2b Binary files /dev/null and b/tts/cache/51883c9652913092deb2a188040eac52.wav differ diff --git a/tts/cache/53c3dca6059ce30bd5a6896b26437313.wav b/tts/cache/53c3dca6059ce30bd5a6896b26437313.wav new file mode 100644 index 0000000..836e0a0 Binary files /dev/null and b/tts/cache/53c3dca6059ce30bd5a6896b26437313.wav differ diff --git a/tts/cache/542357dad73d1253433bc37b63577c5a.wav b/tts/cache/542357dad73d1253433bc37b63577c5a.wav new file mode 100644 index 0000000..51f2467 Binary files /dev/null and b/tts/cache/542357dad73d1253433bc37b63577c5a.wav differ diff --git a/tts/cache/5439e139d2dceb09c7de49d86381897d.wav b/tts/cache/5439e139d2dceb09c7de49d86381897d.wav new file mode 100644 index 0000000..c415d8e Binary files /dev/null and b/tts/cache/5439e139d2dceb09c7de49d86381897d.wav differ diff --git a/tts/cache/548ece3a0de02cd99928da64f4dbbd10.wav b/tts/cache/548ece3a0de02cd99928da64f4dbbd10.wav new file mode 100644 index 0000000..1be9e06 Binary files /dev/null and b/tts/cache/548ece3a0de02cd99928da64f4dbbd10.wav differ diff --git a/tts/cache/54906ae37bd0f72c6303e32c04ec1e63.wav b/tts/cache/54906ae37bd0f72c6303e32c04ec1e63.wav new file mode 100644 index 0000000..ca85b53 Binary files /dev/null and b/tts/cache/54906ae37bd0f72c6303e32c04ec1e63.wav differ diff --git a/tts/cache/54cd3e7a11ff47da3d40605592f787fc.wav b/tts/cache/54cd3e7a11ff47da3d40605592f787fc.wav new file mode 100644 index 0000000..051aed3 Binary files /dev/null and b/tts/cache/54cd3e7a11ff47da3d40605592f787fc.wav differ diff --git a/tts/cache/55b8c6ea7ab787b84b2dcf809e5251bd.wav b/tts/cache/55b8c6ea7ab787b84b2dcf809e5251bd.wav new file mode 100644 index 0000000..476517f Binary files /dev/null and b/tts/cache/55b8c6ea7ab787b84b2dcf809e5251bd.wav differ diff --git a/tts/cache/55faa2a9d5be1dc1460bf4e45cfbf4dc.wav b/tts/cache/55faa2a9d5be1dc1460bf4e45cfbf4dc.wav new file mode 100644 index 0000000..ff70ad1 Binary files /dev/null and b/tts/cache/55faa2a9d5be1dc1460bf4e45cfbf4dc.wav differ diff --git a/tts/cache/56408f10b4ad9022c133677d52cc5a3a.wav b/tts/cache/56408f10b4ad9022c133677d52cc5a3a.wav new file mode 100644 index 0000000..8281c0b Binary files /dev/null and b/tts/cache/56408f10b4ad9022c133677d52cc5a3a.wav differ diff --git a/tts/cache/56a3157d94a28f4a29a2525be21ff5e0.wav b/tts/cache/56a3157d94a28f4a29a2525be21ff5e0.wav new file mode 100644 index 0000000..97f5c02 Binary files /dev/null and b/tts/cache/56a3157d94a28f4a29a2525be21ff5e0.wav differ diff --git a/tts/cache/56dad8002f5de986c4d790eaa62d9a6f.wav b/tts/cache/56dad8002f5de986c4d790eaa62d9a6f.wav new file mode 100644 index 0000000..2795f93 Binary files /dev/null and b/tts/cache/56dad8002f5de986c4d790eaa62d9a6f.wav differ diff --git a/tts/cache/570119128e7bd163c735a1b794a3735b.wav b/tts/cache/570119128e7bd163c735a1b794a3735b.wav new file mode 100644 index 0000000..04767a3 Binary files /dev/null and b/tts/cache/570119128e7bd163c735a1b794a3735b.wav differ diff --git a/tts/cache/5762e98281a5716783438ae34eeaab0a.wav b/tts/cache/5762e98281a5716783438ae34eeaab0a.wav new file mode 100644 index 0000000..4b6e4aa Binary files /dev/null and b/tts/cache/5762e98281a5716783438ae34eeaab0a.wav differ diff --git a/tts/cache/579764b58920578566824235b7890a65.wav b/tts/cache/579764b58920578566824235b7890a65.wav new file mode 100644 index 0000000..60b4d35 Binary files /dev/null and b/tts/cache/579764b58920578566824235b7890a65.wav differ diff --git a/tts/cache/57de9ba5b7a8c5af485d9936b6d52bcf.wav b/tts/cache/57de9ba5b7a8c5af485d9936b6d52bcf.wav new file mode 100644 index 0000000..7f619e7 Binary files /dev/null and b/tts/cache/57de9ba5b7a8c5af485d9936b6d52bcf.wav differ diff --git a/tts/cache/582d15a8f368d35fac33648d044ac88b.wav b/tts/cache/582d15a8f368d35fac33648d044ac88b.wav new file mode 100644 index 0000000..ce51e5c Binary files /dev/null and b/tts/cache/582d15a8f368d35fac33648d044ac88b.wav differ diff --git a/tts/cache/59517b097e43308cdda27408935e76ab.wav b/tts/cache/59517b097e43308cdda27408935e76ab.wav new file mode 100644 index 0000000..f6fd925 Binary files /dev/null and b/tts/cache/59517b097e43308cdda27408935e76ab.wav differ diff --git a/tts/cache/59cc7cff35d8f2d4cb83ced7c10c61c3.wav b/tts/cache/59cc7cff35d8f2d4cb83ced7c10c61c3.wav new file mode 100644 index 0000000..3b240a8 Binary files /dev/null and b/tts/cache/59cc7cff35d8f2d4cb83ced7c10c61c3.wav differ diff --git a/tts/cache/5a0e488333002204edf7a7e051664b61.wav b/tts/cache/5a0e488333002204edf7a7e051664b61.wav new file mode 100644 index 0000000..fe268c2 Binary files /dev/null and b/tts/cache/5a0e488333002204edf7a7e051664b61.wav differ diff --git a/tts/cache/5a17cf34873206eed4a66f6ca8c31eb4.wav b/tts/cache/5a17cf34873206eed4a66f6ca8c31eb4.wav new file mode 100644 index 0000000..b9832bf Binary files /dev/null and b/tts/cache/5a17cf34873206eed4a66f6ca8c31eb4.wav differ diff --git a/tts/cache/5a39d6676cf0f8a82b7f54ea80fdb171.wav b/tts/cache/5a39d6676cf0f8a82b7f54ea80fdb171.wav new file mode 100644 index 0000000..f3fbeba Binary files /dev/null and b/tts/cache/5a39d6676cf0f8a82b7f54ea80fdb171.wav differ diff --git a/tts/cache/5afa6bb8071a40079dd125f1b9b50b95.wav b/tts/cache/5afa6bb8071a40079dd125f1b9b50b95.wav new file mode 100644 index 0000000..1a136f5 Binary files /dev/null and b/tts/cache/5afa6bb8071a40079dd125f1b9b50b95.wav differ diff --git a/tts/cache/5c02710ae3aa40bd5a4530e357c0c868.wav b/tts/cache/5c02710ae3aa40bd5a4530e357c0c868.wav new file mode 100644 index 0000000..36b8123 Binary files /dev/null and b/tts/cache/5c02710ae3aa40bd5a4530e357c0c868.wav differ diff --git a/tts/cache/5c0eca30842fb81f599f368cfe9df6d9.wav b/tts/cache/5c0eca30842fb81f599f368cfe9df6d9.wav new file mode 100644 index 0000000..9bb2768 Binary files /dev/null and b/tts/cache/5c0eca30842fb81f599f368cfe9df6d9.wav differ diff --git a/tts/cache/5cba30abb477bedf350866af25077b0c.wav b/tts/cache/5cba30abb477bedf350866af25077b0c.wav new file mode 100644 index 0000000..a9744b6 Binary files /dev/null and b/tts/cache/5cba30abb477bedf350866af25077b0c.wav differ diff --git a/tts/cache/5dbf4746af086796c5745f6bc4a9f1ae.wav b/tts/cache/5dbf4746af086796c5745f6bc4a9f1ae.wav new file mode 100644 index 0000000..aeee424 Binary files /dev/null and b/tts/cache/5dbf4746af086796c5745f6bc4a9f1ae.wav differ diff --git a/tts/cache/5dc226c4aaa7c77f67e5bcdbc1b25961.wav b/tts/cache/5dc226c4aaa7c77f67e5bcdbc1b25961.wav new file mode 100644 index 0000000..2034ec6 Binary files /dev/null and b/tts/cache/5dc226c4aaa7c77f67e5bcdbc1b25961.wav differ diff --git a/tts/cache/5e3460083948f1bf86da7378f45469a3.wav b/tts/cache/5e3460083948f1bf86da7378f45469a3.wav new file mode 100644 index 0000000..1dd91c0 Binary files /dev/null and b/tts/cache/5e3460083948f1bf86da7378f45469a3.wav differ diff --git a/tts/cache/5ee3144de051e1aca95283564d7aca78.wav b/tts/cache/5ee3144de051e1aca95283564d7aca78.wav new file mode 100644 index 0000000..1bd1a88 Binary files /dev/null and b/tts/cache/5ee3144de051e1aca95283564d7aca78.wav differ diff --git a/tts/cache/5f4d168b99b597f45980803dbccdcef6.wav b/tts/cache/5f4d168b99b597f45980803dbccdcef6.wav new file mode 100644 index 0000000..9e51375 Binary files /dev/null and b/tts/cache/5f4d168b99b597f45980803dbccdcef6.wav differ diff --git a/tts/cache/60b204db1082477317fe0b273cb50aa7.wav b/tts/cache/60b204db1082477317fe0b273cb50aa7.wav new file mode 100644 index 0000000..0b371e2 Binary files /dev/null and b/tts/cache/60b204db1082477317fe0b273cb50aa7.wav differ diff --git a/tts/cache/60f5ee976eaf924a710faf21be0cc8d4.wav b/tts/cache/60f5ee976eaf924a710faf21be0cc8d4.wav new file mode 100644 index 0000000..fa8ecf2 Binary files /dev/null and b/tts/cache/60f5ee976eaf924a710faf21be0cc8d4.wav differ diff --git a/tts/cache/612d60c0b6e5e655e47d8d27c7350db9.wav b/tts/cache/612d60c0b6e5e655e47d8d27c7350db9.wav new file mode 100644 index 0000000..9bb6ff7 Binary files /dev/null and b/tts/cache/612d60c0b6e5e655e47d8d27c7350db9.wav differ diff --git a/tts/cache/6130b4142474f3b16b08a74301fad447.wav b/tts/cache/6130b4142474f3b16b08a74301fad447.wav new file mode 100644 index 0000000..da23942 Binary files /dev/null and b/tts/cache/6130b4142474f3b16b08a74301fad447.wav differ diff --git a/tts/cache/6138363529d685cf261f5e3ac1c9cc59.wav b/tts/cache/6138363529d685cf261f5e3ac1c9cc59.wav new file mode 100644 index 0000000..9db9fb3 Binary files /dev/null and b/tts/cache/6138363529d685cf261f5e3ac1c9cc59.wav differ diff --git a/tts/cache/6141a4c00b94f9a56f4a2c215135482d.wav b/tts/cache/6141a4c00b94f9a56f4a2c215135482d.wav new file mode 100644 index 0000000..4f9f548 Binary files /dev/null and b/tts/cache/6141a4c00b94f9a56f4a2c215135482d.wav differ diff --git a/tts/cache/6154ca86f7f0780e625c7fa8969a49cf.wav b/tts/cache/6154ca86f7f0780e625c7fa8969a49cf.wav new file mode 100644 index 0000000..f3298c6 Binary files /dev/null and b/tts/cache/6154ca86f7f0780e625c7fa8969a49cf.wav differ diff --git a/tts/cache/6167dbb88f98c12d26b6f73af2c40ec0.wav b/tts/cache/6167dbb88f98c12d26b6f73af2c40ec0.wav new file mode 100644 index 0000000..67ff3e1 Binary files /dev/null and b/tts/cache/6167dbb88f98c12d26b6f73af2c40ec0.wav differ diff --git a/tts/cache/616b795866fb7d1ff710ef4ba664e91d.wav b/tts/cache/616b795866fb7d1ff710ef4ba664e91d.wav new file mode 100644 index 0000000..3f853ce Binary files /dev/null and b/tts/cache/616b795866fb7d1ff710ef4ba664e91d.wav differ diff --git a/tts/cache/61c83470eff88e932c72c405ecb53b14.wav b/tts/cache/61c83470eff88e932c72c405ecb53b14.wav new file mode 100644 index 0000000..a1eba7d Binary files /dev/null and b/tts/cache/61c83470eff88e932c72c405ecb53b14.wav differ diff --git a/tts/cache/61fba876ee6896250414b54b948d3f7c.wav b/tts/cache/61fba876ee6896250414b54b948d3f7c.wav new file mode 100644 index 0000000..18f7061 Binary files /dev/null and b/tts/cache/61fba876ee6896250414b54b948d3f7c.wav differ diff --git a/tts/cache/622ba6bc9a6238523a161b66fbe0c8fb.wav b/tts/cache/622ba6bc9a6238523a161b66fbe0c8fb.wav new file mode 100644 index 0000000..8efb72d Binary files /dev/null and b/tts/cache/622ba6bc9a6238523a161b66fbe0c8fb.wav differ diff --git a/tts/cache/62a29a5d3540f24fbd642547e04354d8.wav b/tts/cache/62a29a5d3540f24fbd642547e04354d8.wav new file mode 100644 index 0000000..ac6532c Binary files /dev/null and b/tts/cache/62a29a5d3540f24fbd642547e04354d8.wav differ diff --git a/tts/cache/62bebebe42615c706e9e299fdc037c23.wav b/tts/cache/62bebebe42615c706e9e299fdc037c23.wav new file mode 100644 index 0000000..aece363 Binary files /dev/null and b/tts/cache/62bebebe42615c706e9e299fdc037c23.wav differ diff --git a/tts/cache/64ce1ca4233cc748ac5d26c99236e924.wav b/tts/cache/64ce1ca4233cc748ac5d26c99236e924.wav new file mode 100644 index 0000000..81d37ab Binary files /dev/null and b/tts/cache/64ce1ca4233cc748ac5d26c99236e924.wav differ diff --git a/tts/cache/6557c09d9f4f47634fc48c7c72303336.wav b/tts/cache/6557c09d9f4f47634fc48c7c72303336.wav new file mode 100644 index 0000000..51e4fe2 Binary files /dev/null and b/tts/cache/6557c09d9f4f47634fc48c7c72303336.wav differ diff --git a/tts/cache/65c7debe90ea99815814cbef1b7da11f.wav b/tts/cache/65c7debe90ea99815814cbef1b7da11f.wav new file mode 100644 index 0000000..7ce9c31 Binary files /dev/null and b/tts/cache/65c7debe90ea99815814cbef1b7da11f.wav differ diff --git a/tts/cache/662bed8f34d9c8db3362a5fbaf49db1f.wav b/tts/cache/662bed8f34d9c8db3362a5fbaf49db1f.wav new file mode 100644 index 0000000..0b9fcb1 Binary files /dev/null and b/tts/cache/662bed8f34d9c8db3362a5fbaf49db1f.wav differ diff --git a/tts/cache/68a75652988ee8bbaff89a52d72ca670.wav b/tts/cache/68a75652988ee8bbaff89a52d72ca670.wav new file mode 100644 index 0000000..f6611e6 Binary files /dev/null and b/tts/cache/68a75652988ee8bbaff89a52d72ca670.wav differ diff --git a/tts/cache/68d603183d4368dacbfb527293a5de36.wav b/tts/cache/68d603183d4368dacbfb527293a5de36.wav new file mode 100644 index 0000000..263fedb Binary files /dev/null and b/tts/cache/68d603183d4368dacbfb527293a5de36.wav differ diff --git a/tts/cache/6a6d389c4b746ef3c227a1c128147c4e.wav b/tts/cache/6a6d389c4b746ef3c227a1c128147c4e.wav new file mode 100644 index 0000000..0afeb8b Binary files /dev/null and b/tts/cache/6a6d389c4b746ef3c227a1c128147c4e.wav differ diff --git a/tts/cache/6b7898bf317860b708995e39ec5444ce.wav b/tts/cache/6b7898bf317860b708995e39ec5444ce.wav new file mode 100644 index 0000000..29dede1 Binary files /dev/null and b/tts/cache/6b7898bf317860b708995e39ec5444ce.wav differ diff --git a/tts/cache/6bb16be0f7ecbb16a736465cee123093.wav b/tts/cache/6bb16be0f7ecbb16a736465cee123093.wav new file mode 100644 index 0000000..50c5ad0 Binary files /dev/null and b/tts/cache/6bb16be0f7ecbb16a736465cee123093.wav differ diff --git a/tts/cache/6d45f4cac9241ad8528bb5b14ddcd9b4.wav b/tts/cache/6d45f4cac9241ad8528bb5b14ddcd9b4.wav new file mode 100644 index 0000000..414fbf1 Binary files /dev/null and b/tts/cache/6d45f4cac9241ad8528bb5b14ddcd9b4.wav differ diff --git a/tts/cache/6e0a5a440780f3d8cfb8ff9e4d089e0c.wav b/tts/cache/6e0a5a440780f3d8cfb8ff9e4d089e0c.wav new file mode 100644 index 0000000..c2ee0da Binary files /dev/null and b/tts/cache/6e0a5a440780f3d8cfb8ff9e4d089e0c.wav differ diff --git a/tts/cache/6e7c23505e7f02381c653ddb459ed62e.wav b/tts/cache/6e7c23505e7f02381c653ddb459ed62e.wav new file mode 100644 index 0000000..bb3ee64 Binary files /dev/null and b/tts/cache/6e7c23505e7f02381c653ddb459ed62e.wav differ diff --git a/tts/cache/6eedabfd10843358dfc08b69d8a68128.wav b/tts/cache/6eedabfd10843358dfc08b69d8a68128.wav new file mode 100644 index 0000000..f72d732 Binary files /dev/null and b/tts/cache/6eedabfd10843358dfc08b69d8a68128.wav differ diff --git a/tts/cache/6f447cd1c3b20262f2bd21064818c925.wav b/tts/cache/6f447cd1c3b20262f2bd21064818c925.wav new file mode 100644 index 0000000..6e79930 Binary files /dev/null and b/tts/cache/6f447cd1c3b20262f2bd21064818c925.wav differ diff --git a/tts/cache/6f48c809c02e28168ba9754cdea334c6.wav b/tts/cache/6f48c809c02e28168ba9754cdea334c6.wav new file mode 100644 index 0000000..745ceff Binary files /dev/null and b/tts/cache/6f48c809c02e28168ba9754cdea334c6.wav differ diff --git a/tts/cache/6f7f9590fd9b71526667bafc02bf4c09.wav b/tts/cache/6f7f9590fd9b71526667bafc02bf4c09.wav new file mode 100644 index 0000000..7e30e43 Binary files /dev/null and b/tts/cache/6f7f9590fd9b71526667bafc02bf4c09.wav differ diff --git a/tts/cache/6ff29901d49f3dab09f3a17a1e276277.wav b/tts/cache/6ff29901d49f3dab09f3a17a1e276277.wav new file mode 100644 index 0000000..268e6ec Binary files /dev/null and b/tts/cache/6ff29901d49f3dab09f3a17a1e276277.wav differ diff --git a/tts/cache/7045e84dda3414c681654bbd6b40e5a9.wav b/tts/cache/7045e84dda3414c681654bbd6b40e5a9.wav new file mode 100644 index 0000000..ecc51e2 Binary files /dev/null and b/tts/cache/7045e84dda3414c681654bbd6b40e5a9.wav differ diff --git a/tts/cache/70fb628cce7db7739ea154de2d719c69.wav b/tts/cache/70fb628cce7db7739ea154de2d719c69.wav new file mode 100644 index 0000000..9d57d6f Binary files /dev/null and b/tts/cache/70fb628cce7db7739ea154de2d719c69.wav differ diff --git a/tts/cache/71709e55f55cc2b35b6bec6ee379f8b2.wav b/tts/cache/71709e55f55cc2b35b6bec6ee379f8b2.wav new file mode 100644 index 0000000..2910f0a Binary files /dev/null and b/tts/cache/71709e55f55cc2b35b6bec6ee379f8b2.wav differ diff --git a/tts/cache/71da7f41c5b636ff1514b9dda6cc3aad.wav b/tts/cache/71da7f41c5b636ff1514b9dda6cc3aad.wav new file mode 100644 index 0000000..72b7ba4 Binary files /dev/null and b/tts/cache/71da7f41c5b636ff1514b9dda6cc3aad.wav differ diff --git a/tts/cache/7200dbe231a6c0b76f85fe30bdb478e9.wav b/tts/cache/7200dbe231a6c0b76f85fe30bdb478e9.wav new file mode 100644 index 0000000..b8f9bd4 Binary files /dev/null and b/tts/cache/7200dbe231a6c0b76f85fe30bdb478e9.wav differ diff --git a/tts/cache/721a109e57d403201761427f523ba9ae.wav b/tts/cache/721a109e57d403201761427f523ba9ae.wav new file mode 100644 index 0000000..b7d69bb Binary files /dev/null and b/tts/cache/721a109e57d403201761427f523ba9ae.wav differ diff --git a/tts/cache/7261ebd7ee0522366a4aaa6e14ce5293.wav b/tts/cache/7261ebd7ee0522366a4aaa6e14ce5293.wav new file mode 100644 index 0000000..3a03db8 Binary files /dev/null and b/tts/cache/7261ebd7ee0522366a4aaa6e14ce5293.wav differ diff --git a/tts/cache/726f892b2d8f9b01e36c3ae1d6d35572.wav b/tts/cache/726f892b2d8f9b01e36c3ae1d6d35572.wav new file mode 100644 index 0000000..81b0533 Binary files /dev/null and b/tts/cache/726f892b2d8f9b01e36c3ae1d6d35572.wav differ diff --git a/tts/cache/727a7d36290ac0448ec3ac4dedf7c933.wav b/tts/cache/727a7d36290ac0448ec3ac4dedf7c933.wav new file mode 100644 index 0000000..d775c1c Binary files /dev/null and b/tts/cache/727a7d36290ac0448ec3ac4dedf7c933.wav differ diff --git a/tts/cache/72916169a5bbcdf0b8c1d4ed02bdd32c.wav b/tts/cache/72916169a5bbcdf0b8c1d4ed02bdd32c.wav new file mode 100644 index 0000000..3ae2218 Binary files /dev/null and b/tts/cache/72916169a5bbcdf0b8c1d4ed02bdd32c.wav differ diff --git a/tts/cache/72ed7aa88a1a529977cdace3f6035926.wav b/tts/cache/72ed7aa88a1a529977cdace3f6035926.wav new file mode 100644 index 0000000..8d47bbf Binary files /dev/null and b/tts/cache/72ed7aa88a1a529977cdace3f6035926.wav differ diff --git a/tts/cache/73b216fe46ebfb9bd8f4b79062a7d0bc.wav b/tts/cache/73b216fe46ebfb9bd8f4b79062a7d0bc.wav new file mode 100644 index 0000000..dc72fe0 Binary files /dev/null and b/tts/cache/73b216fe46ebfb9bd8f4b79062a7d0bc.wav differ diff --git a/tts/cache/747decc5d9bfe839e1f94784e59c9f3d.wav b/tts/cache/747decc5d9bfe839e1f94784e59c9f3d.wav new file mode 100644 index 0000000..2639bc5 Binary files /dev/null and b/tts/cache/747decc5d9bfe839e1f94784e59c9f3d.wav differ diff --git a/tts/cache/749aa011a4b374d115df69bcb07392d6.wav b/tts/cache/749aa011a4b374d115df69bcb07392d6.wav new file mode 100644 index 0000000..b8ad1a4 Binary files /dev/null and b/tts/cache/749aa011a4b374d115df69bcb07392d6.wav differ diff --git a/tts/cache/75a7570a7f97dbbd0cf9227e6f04f8d0.wav b/tts/cache/75a7570a7f97dbbd0cf9227e6f04f8d0.wav new file mode 100644 index 0000000..1ae95b3 Binary files /dev/null and b/tts/cache/75a7570a7f97dbbd0cf9227e6f04f8d0.wav differ diff --git a/tts/cache/760616b7f6002bec26ecda592b62f332.wav b/tts/cache/760616b7f6002bec26ecda592b62f332.wav new file mode 100644 index 0000000..b27c07d Binary files /dev/null and b/tts/cache/760616b7f6002bec26ecda592b62f332.wav differ diff --git a/tts/cache/76170229536b44dab58c4a69338e8e98.wav b/tts/cache/76170229536b44dab58c4a69338e8e98.wav new file mode 100644 index 0000000..61acc86 Binary files /dev/null and b/tts/cache/76170229536b44dab58c4a69338e8e98.wav differ diff --git a/tts/cache/7625b3c7c46851ef0905d095ba1cd300.wav b/tts/cache/7625b3c7c46851ef0905d095ba1cd300.wav new file mode 100644 index 0000000..0fa01f0 Binary files /dev/null and b/tts/cache/7625b3c7c46851ef0905d095ba1cd300.wav differ diff --git a/tts/cache/76706c9164eb41fc599e35dd69f7565e.wav b/tts/cache/76706c9164eb41fc599e35dd69f7565e.wav new file mode 100644 index 0000000..8097aa1 Binary files /dev/null and b/tts/cache/76706c9164eb41fc599e35dd69f7565e.wav differ diff --git a/tts/cache/768dc1a3cf87082312e7fbefb2263daa.wav b/tts/cache/768dc1a3cf87082312e7fbefb2263daa.wav new file mode 100644 index 0000000..0d6c152 Binary files /dev/null and b/tts/cache/768dc1a3cf87082312e7fbefb2263daa.wav differ diff --git a/tts/cache/779dd163ef27db10032c11cceee7c8a0.wav b/tts/cache/779dd163ef27db10032c11cceee7c8a0.wav new file mode 100644 index 0000000..7816f3f Binary files /dev/null and b/tts/cache/779dd163ef27db10032c11cceee7c8a0.wav differ diff --git a/tts/cache/77a34b6fbc44b2ade8017cf519bb3f39.wav b/tts/cache/77a34b6fbc44b2ade8017cf519bb3f39.wav new file mode 100644 index 0000000..f08e21f Binary files /dev/null and b/tts/cache/77a34b6fbc44b2ade8017cf519bb3f39.wav differ diff --git a/tts/cache/77ff2e22cf745d71d22005541e8160bd.wav b/tts/cache/77ff2e22cf745d71d22005541e8160bd.wav new file mode 100644 index 0000000..3231b9b Binary files /dev/null and b/tts/cache/77ff2e22cf745d71d22005541e8160bd.wav differ diff --git a/tts/cache/78088eb60fe12244137612cf6c988307.wav b/tts/cache/78088eb60fe12244137612cf6c988307.wav new file mode 100644 index 0000000..2625512 Binary files /dev/null and b/tts/cache/78088eb60fe12244137612cf6c988307.wav differ diff --git a/tts/cache/78a5dc56e0e3bc5209b08703d2455c1a.wav b/tts/cache/78a5dc56e0e3bc5209b08703d2455c1a.wav new file mode 100644 index 0000000..bf6413c Binary files /dev/null and b/tts/cache/78a5dc56e0e3bc5209b08703d2455c1a.wav differ diff --git a/tts/cache/78e75331d45f3dd8a86340bfdc045b28.wav b/tts/cache/78e75331d45f3dd8a86340bfdc045b28.wav new file mode 100644 index 0000000..744812c Binary files /dev/null and b/tts/cache/78e75331d45f3dd8a86340bfdc045b28.wav differ diff --git a/tts/cache/7922e9e86a98bfc33d3d29e042d56080.wav b/tts/cache/7922e9e86a98bfc33d3d29e042d56080.wav new file mode 100644 index 0000000..c2fbaa0 Binary files /dev/null and b/tts/cache/7922e9e86a98bfc33d3d29e042d56080.wav differ diff --git a/tts/cache/79609a8254bec058c2d04bcf2cfe3916.wav b/tts/cache/79609a8254bec058c2d04bcf2cfe3916.wav new file mode 100644 index 0000000..352b909 Binary files /dev/null and b/tts/cache/79609a8254bec058c2d04bcf2cfe3916.wav differ diff --git a/tts/cache/799baed6e4cff053f2fa0fbb699497e7.wav b/tts/cache/799baed6e4cff053f2fa0fbb699497e7.wav new file mode 100644 index 0000000..82a5d92 Binary files /dev/null and b/tts/cache/799baed6e4cff053f2fa0fbb699497e7.wav differ diff --git a/tts/cache/79dc9f8700087e05fb87e6d4d8dcaadc.wav b/tts/cache/79dc9f8700087e05fb87e6d4d8dcaadc.wav new file mode 100644 index 0000000..9c4e454 Binary files /dev/null and b/tts/cache/79dc9f8700087e05fb87e6d4d8dcaadc.wav differ diff --git a/tts/cache/7a882850865740cf3a5d0d27f004ff08.wav b/tts/cache/7a882850865740cf3a5d0d27f004ff08.wav new file mode 100644 index 0000000..61f3ab9 Binary files /dev/null and b/tts/cache/7a882850865740cf3a5d0d27f004ff08.wav differ diff --git a/tts/cache/7b07eb5c986f96bf35a8c594d2ac15fd.wav b/tts/cache/7b07eb5c986f96bf35a8c594d2ac15fd.wav new file mode 100644 index 0000000..c6504ea Binary files /dev/null and b/tts/cache/7b07eb5c986f96bf35a8c594d2ac15fd.wav differ diff --git a/tts/cache/7b1c8ad40c3ec6c8c8e2c91f153b1215.wav b/tts/cache/7b1c8ad40c3ec6c8c8e2c91f153b1215.wav new file mode 100644 index 0000000..4a96118 Binary files /dev/null and b/tts/cache/7b1c8ad40c3ec6c8c8e2c91f153b1215.wav differ diff --git a/tts/cache/7b5eb905ee84183dedc62f035658359a.wav b/tts/cache/7b5eb905ee84183dedc62f035658359a.wav new file mode 100644 index 0000000..d26ab40 Binary files /dev/null and b/tts/cache/7b5eb905ee84183dedc62f035658359a.wav differ diff --git a/tts/cache/7ba43cd09978ea6a9d9b0dca4e350435.wav b/tts/cache/7ba43cd09978ea6a9d9b0dca4e350435.wav new file mode 100644 index 0000000..3fb84f9 Binary files /dev/null and b/tts/cache/7ba43cd09978ea6a9d9b0dca4e350435.wav differ diff --git a/tts/cache/7be60c6ead0241cf5440084a34da1690.wav b/tts/cache/7be60c6ead0241cf5440084a34da1690.wav new file mode 100644 index 0000000..22dd919 Binary files /dev/null and b/tts/cache/7be60c6ead0241cf5440084a34da1690.wav differ diff --git a/tts/cache/7c3c52bb022e1332850344bac7cedfcd.wav b/tts/cache/7c3c52bb022e1332850344bac7cedfcd.wav new file mode 100644 index 0000000..e69f4b4 Binary files /dev/null and b/tts/cache/7c3c52bb022e1332850344bac7cedfcd.wav differ diff --git a/tts/cache/7c9acf51af19bd5023e332cfc054a38f.wav b/tts/cache/7c9acf51af19bd5023e332cfc054a38f.wav new file mode 100644 index 0000000..4219f4d Binary files /dev/null and b/tts/cache/7c9acf51af19bd5023e332cfc054a38f.wav differ diff --git a/tts/cache/7cd5e9c525791e4a0fcf49c3e8e501ba.wav b/tts/cache/7cd5e9c525791e4a0fcf49c3e8e501ba.wav new file mode 100644 index 0000000..1ecfafe Binary files /dev/null and b/tts/cache/7cd5e9c525791e4a0fcf49c3e8e501ba.wav differ diff --git a/tts/cache/7d48efe50ea36d451ab0cb2ce6923a40.wav b/tts/cache/7d48efe50ea36d451ab0cb2ce6923a40.wav new file mode 100644 index 0000000..ee57a6b Binary files /dev/null and b/tts/cache/7d48efe50ea36d451ab0cb2ce6923a40.wav differ diff --git a/tts/cache/8040b37d6502dcb12f6b7f6ccde0eb90.wav b/tts/cache/8040b37d6502dcb12f6b7f6ccde0eb90.wav new file mode 100644 index 0000000..f8290c9 Binary files /dev/null and b/tts/cache/8040b37d6502dcb12f6b7f6ccde0eb90.wav differ diff --git a/tts/cache/804185121e09f46f97fcefd8d355111f.wav b/tts/cache/804185121e09f46f97fcefd8d355111f.wav new file mode 100644 index 0000000..de75a0d Binary files /dev/null and b/tts/cache/804185121e09f46f97fcefd8d355111f.wav differ diff --git a/tts/cache/80b061ea1f263aced45f8f44bcf8f5c4.wav b/tts/cache/80b061ea1f263aced45f8f44bcf8f5c4.wav new file mode 100644 index 0000000..f68f87f Binary files /dev/null and b/tts/cache/80b061ea1f263aced45f8f44bcf8f5c4.wav differ diff --git a/tts/cache/80e3ddbdbed19fec6d8f98ff2b86044d.wav b/tts/cache/80e3ddbdbed19fec6d8f98ff2b86044d.wav new file mode 100644 index 0000000..6af3c93 Binary files /dev/null and b/tts/cache/80e3ddbdbed19fec6d8f98ff2b86044d.wav differ diff --git a/tts/cache/81005acea985cba4829b0cbd307a985e.wav b/tts/cache/81005acea985cba4829b0cbd307a985e.wav new file mode 100644 index 0000000..f0bf8c3 Binary files /dev/null and b/tts/cache/81005acea985cba4829b0cbd307a985e.wav differ diff --git a/tts/cache/81081044e6e0d25f03711e0fe6d2d151.wav b/tts/cache/81081044e6e0d25f03711e0fe6d2d151.wav new file mode 100644 index 0000000..9e7ef2e Binary files /dev/null and b/tts/cache/81081044e6e0d25f03711e0fe6d2d151.wav differ diff --git a/tts/cache/810b6625a1546b6abc979d27fa3c3974.wav b/tts/cache/810b6625a1546b6abc979d27fa3c3974.wav new file mode 100644 index 0000000..2f1abcb Binary files /dev/null and b/tts/cache/810b6625a1546b6abc979d27fa3c3974.wav differ diff --git a/tts/cache/8113be4f4949a6f32a1863f0d4540076.wav b/tts/cache/8113be4f4949a6f32a1863f0d4540076.wav new file mode 100644 index 0000000..32009df Binary files /dev/null and b/tts/cache/8113be4f4949a6f32a1863f0d4540076.wav differ diff --git a/tts/cache/81e562036a43d6c0fead97fadd0051e6.wav b/tts/cache/81e562036a43d6c0fead97fadd0051e6.wav new file mode 100644 index 0000000..0af6669 Binary files /dev/null and b/tts/cache/81e562036a43d6c0fead97fadd0051e6.wav differ diff --git a/tts/cache/82052c3034f3bd1903869c64c9812b2e.wav b/tts/cache/82052c3034f3bd1903869c64c9812b2e.wav new file mode 100644 index 0000000..4f1bed2 Binary files /dev/null and b/tts/cache/82052c3034f3bd1903869c64c9812b2e.wav differ diff --git a/tts/cache/8345c562d0654a1f4ce761bf4555fc12.wav b/tts/cache/8345c562d0654a1f4ce761bf4555fc12.wav new file mode 100644 index 0000000..c50afc7 Binary files /dev/null and b/tts/cache/8345c562d0654a1f4ce761bf4555fc12.wav differ diff --git a/tts/cache/83a226fabd8554c83d8cee81d91666d6.wav b/tts/cache/83a226fabd8554c83d8cee81d91666d6.wav new file mode 100644 index 0000000..13a064a Binary files /dev/null and b/tts/cache/83a226fabd8554c83d8cee81d91666d6.wav differ diff --git a/tts/cache/83acc177c75f7679bffff55db3ced55e.wav b/tts/cache/83acc177c75f7679bffff55db3ced55e.wav new file mode 100644 index 0000000..e29a5f0 Binary files /dev/null and b/tts/cache/83acc177c75f7679bffff55db3ced55e.wav differ diff --git a/tts/cache/848f2e26a49d0e66edb39a492e601ee4.wav b/tts/cache/848f2e26a49d0e66edb39a492e601ee4.wav new file mode 100644 index 0000000..f65bbe9 Binary files /dev/null and b/tts/cache/848f2e26a49d0e66edb39a492e601ee4.wav differ diff --git a/tts/cache/84d8ac107400776efee18ae2891b2c62.wav b/tts/cache/84d8ac107400776efee18ae2891b2c62.wav new file mode 100644 index 0000000..36dd4df Binary files /dev/null and b/tts/cache/84d8ac107400776efee18ae2891b2c62.wav differ diff --git a/tts/cache/84ff31beb0ff2800546caaa3e952334f.wav b/tts/cache/84ff31beb0ff2800546caaa3e952334f.wav new file mode 100644 index 0000000..8ed5506 Binary files /dev/null and b/tts/cache/84ff31beb0ff2800546caaa3e952334f.wav differ diff --git a/tts/cache/8546fa4c631288d930d824cd0264d7fc.wav b/tts/cache/8546fa4c631288d930d824cd0264d7fc.wav new file mode 100644 index 0000000..5ae6aac Binary files /dev/null and b/tts/cache/8546fa4c631288d930d824cd0264d7fc.wav differ diff --git a/tts/cache/85687c4acc390638b5baf54cb39e7548.wav b/tts/cache/85687c4acc390638b5baf54cb39e7548.wav new file mode 100644 index 0000000..9b5587f Binary files /dev/null and b/tts/cache/85687c4acc390638b5baf54cb39e7548.wav differ diff --git a/tts/cache/85aef2769027ec5e62655d82dc0eb7ea.wav b/tts/cache/85aef2769027ec5e62655d82dc0eb7ea.wav new file mode 100644 index 0000000..8215881 Binary files /dev/null and b/tts/cache/85aef2769027ec5e62655d82dc0eb7ea.wav differ diff --git a/tts/cache/8615bf8239041b544dcafdabadf13c05.wav b/tts/cache/8615bf8239041b544dcafdabadf13c05.wav new file mode 100644 index 0000000..7a57b21 Binary files /dev/null and b/tts/cache/8615bf8239041b544dcafdabadf13c05.wav differ diff --git a/tts/cache/86526b0cc8e6fcd43c82c48b1b51e974.wav b/tts/cache/86526b0cc8e6fcd43c82c48b1b51e974.wav new file mode 100644 index 0000000..1aa5348 Binary files /dev/null and b/tts/cache/86526b0cc8e6fcd43c82c48b1b51e974.wav differ diff --git a/tts/cache/869bcaa33531d7e93ebf3209dd82dbb9.wav b/tts/cache/869bcaa33531d7e93ebf3209dd82dbb9.wav new file mode 100644 index 0000000..29dcf38 Binary files /dev/null and b/tts/cache/869bcaa33531d7e93ebf3209dd82dbb9.wav differ diff --git a/tts/cache/86c851fa63ce13c05e4ad3903262264f.wav b/tts/cache/86c851fa63ce13c05e4ad3903262264f.wav new file mode 100644 index 0000000..89279fb Binary files /dev/null and b/tts/cache/86c851fa63ce13c05e4ad3903262264f.wav differ diff --git a/tts/cache/87a1840048d89bd0e70b4c320f323a3a.wav b/tts/cache/87a1840048d89bd0e70b4c320f323a3a.wav new file mode 100644 index 0000000..160f001 Binary files /dev/null and b/tts/cache/87a1840048d89bd0e70b4c320f323a3a.wav differ diff --git a/tts/cache/87b3e287c76c095c725cd00834f3f83e.wav b/tts/cache/87b3e287c76c095c725cd00834f3f83e.wav new file mode 100644 index 0000000..d790cc5 Binary files /dev/null and b/tts/cache/87b3e287c76c095c725cd00834f3f83e.wav differ diff --git a/tts/cache/884eb28b15b6dacda316617f1e4ccb20.wav b/tts/cache/884eb28b15b6dacda316617f1e4ccb20.wav new file mode 100644 index 0000000..8b2004f Binary files /dev/null and b/tts/cache/884eb28b15b6dacda316617f1e4ccb20.wav differ diff --git a/tts/cache/888bc961487b0e854b0594e51f989853.wav b/tts/cache/888bc961487b0e854b0594e51f989853.wav new file mode 100644 index 0000000..6d989a6 Binary files /dev/null and b/tts/cache/888bc961487b0e854b0594e51f989853.wav differ diff --git a/tts/cache/88aad27158350bc37b431e2d70d43056.wav b/tts/cache/88aad27158350bc37b431e2d70d43056.wav new file mode 100644 index 0000000..bfb65f2 Binary files /dev/null and b/tts/cache/88aad27158350bc37b431e2d70d43056.wav differ diff --git a/tts/cache/88b4a6b49b347d5f7ac9b945af7a75dd.wav b/tts/cache/88b4a6b49b347d5f7ac9b945af7a75dd.wav new file mode 100644 index 0000000..22aff20 Binary files /dev/null and b/tts/cache/88b4a6b49b347d5f7ac9b945af7a75dd.wav differ diff --git a/tts/cache/88cf096cea965dae2b593ed029015d59.wav b/tts/cache/88cf096cea965dae2b593ed029015d59.wav new file mode 100644 index 0000000..d5572e8 Binary files /dev/null and b/tts/cache/88cf096cea965dae2b593ed029015d59.wav differ diff --git a/tts/cache/88dfb4e7167dc93236fcb79a31edd36b.wav b/tts/cache/88dfb4e7167dc93236fcb79a31edd36b.wav new file mode 100644 index 0000000..7ff9745 Binary files /dev/null and b/tts/cache/88dfb4e7167dc93236fcb79a31edd36b.wav differ diff --git a/tts/cache/890ebb4b7a6281efc1445b2053a5659c.wav b/tts/cache/890ebb4b7a6281efc1445b2053a5659c.wav new file mode 100644 index 0000000..efa5b18 Binary files /dev/null and b/tts/cache/890ebb4b7a6281efc1445b2053a5659c.wav differ diff --git a/tts/cache/89acd74a676987baad77f25a54a4c20d.wav b/tts/cache/89acd74a676987baad77f25a54a4c20d.wav new file mode 100644 index 0000000..baafbda Binary files /dev/null and b/tts/cache/89acd74a676987baad77f25a54a4c20d.wav differ diff --git a/tts/cache/89e0a0af8d7e8a8b4fab87996756ac0a.wav b/tts/cache/89e0a0af8d7e8a8b4fab87996756ac0a.wav new file mode 100644 index 0000000..8da1090 Binary files /dev/null and b/tts/cache/89e0a0af8d7e8a8b4fab87996756ac0a.wav differ diff --git a/tts/cache/8a7b6382f47617d2647c6f3c8f4d3fc1.wav b/tts/cache/8a7b6382f47617d2647c6f3c8f4d3fc1.wav new file mode 100644 index 0000000..4bf0128 Binary files /dev/null and b/tts/cache/8a7b6382f47617d2647c6f3c8f4d3fc1.wav differ diff --git a/tts/cache/8aef62d1f59bae46866e015afcb66bec.wav b/tts/cache/8aef62d1f59bae46866e015afcb66bec.wav new file mode 100644 index 0000000..f9e035e Binary files /dev/null and b/tts/cache/8aef62d1f59bae46866e015afcb66bec.wav differ diff --git a/tts/cache/8b46cba3d04ea6109320ce6248958955.wav b/tts/cache/8b46cba3d04ea6109320ce6248958955.wav new file mode 100644 index 0000000..01cb5b2 Binary files /dev/null and b/tts/cache/8b46cba3d04ea6109320ce6248958955.wav differ diff --git a/tts/cache/8b8314797369876eea278864ce39e246.wav b/tts/cache/8b8314797369876eea278864ce39e246.wav new file mode 100644 index 0000000..c9401fc Binary files /dev/null and b/tts/cache/8b8314797369876eea278864ce39e246.wav differ diff --git a/tts/cache/8bbff7295ffa420ec14070d8dfca840e.wav b/tts/cache/8bbff7295ffa420ec14070d8dfca840e.wav new file mode 100644 index 0000000..c897a8e Binary files /dev/null and b/tts/cache/8bbff7295ffa420ec14070d8dfca840e.wav differ diff --git a/tts/cache/8c5cb6be2a68dcc5defdf5174eaff313.wav b/tts/cache/8c5cb6be2a68dcc5defdf5174eaff313.wav new file mode 100644 index 0000000..9123470 Binary files /dev/null and b/tts/cache/8c5cb6be2a68dcc5defdf5174eaff313.wav differ diff --git a/tts/cache/8d0c821b5ef789046b4dad4e947a23d6.wav b/tts/cache/8d0c821b5ef789046b4dad4e947a23d6.wav new file mode 100644 index 0000000..1931785 Binary files /dev/null and b/tts/cache/8d0c821b5ef789046b4dad4e947a23d6.wav differ diff --git a/tts/cache/8d75f5261af6446e0b3a87e5b4fe3fb5.wav b/tts/cache/8d75f5261af6446e0b3a87e5b4fe3fb5.wav new file mode 100644 index 0000000..5e3d60b Binary files /dev/null and b/tts/cache/8d75f5261af6446e0b3a87e5b4fe3fb5.wav differ diff --git a/tts/cache/8e02d9facd76f041120a251536ad0b31.wav b/tts/cache/8e02d9facd76f041120a251536ad0b31.wav new file mode 100644 index 0000000..b1b6a4a Binary files /dev/null and b/tts/cache/8e02d9facd76f041120a251536ad0b31.wav differ diff --git a/tts/cache/8f2c8f90df0d662ae1f09523cb052697.wav b/tts/cache/8f2c8f90df0d662ae1f09523cb052697.wav new file mode 100644 index 0000000..a645457 Binary files /dev/null and b/tts/cache/8f2c8f90df0d662ae1f09523cb052697.wav differ diff --git a/tts/cache/8fbb52d670affb435a3acb32b641ba1e.wav b/tts/cache/8fbb52d670affb435a3acb32b641ba1e.wav new file mode 100644 index 0000000..1df9fc1 Binary files /dev/null and b/tts/cache/8fbb52d670affb435a3acb32b641ba1e.wav differ diff --git a/tts/cache/9035bb76523949c9287f65426b675248.wav b/tts/cache/9035bb76523949c9287f65426b675248.wav new file mode 100644 index 0000000..c1e23f7 Binary files /dev/null and b/tts/cache/9035bb76523949c9287f65426b675248.wav differ diff --git a/tts/cache/907445f70eb3eb7542e53fd491114936.wav b/tts/cache/907445f70eb3eb7542e53fd491114936.wav new file mode 100644 index 0000000..eb2a26f Binary files /dev/null and b/tts/cache/907445f70eb3eb7542e53fd491114936.wav differ diff --git a/tts/cache/91103cce422ee20d5c83e3ae442ca036.wav b/tts/cache/91103cce422ee20d5c83e3ae442ca036.wav new file mode 100644 index 0000000..7b68207 Binary files /dev/null and b/tts/cache/91103cce422ee20d5c83e3ae442ca036.wav differ diff --git a/tts/cache/913fc62b6e821df9f6b2224057081337.wav b/tts/cache/913fc62b6e821df9f6b2224057081337.wav new file mode 100644 index 0000000..f60fb32 Binary files /dev/null and b/tts/cache/913fc62b6e821df9f6b2224057081337.wav differ diff --git a/tts/cache/9154f0decb3bf9e6965daf0e719e05b5.wav b/tts/cache/9154f0decb3bf9e6965daf0e719e05b5.wav new file mode 100644 index 0000000..81679f2 Binary files /dev/null and b/tts/cache/9154f0decb3bf9e6965daf0e719e05b5.wav differ diff --git a/tts/cache/9163f160a09185a3662f07d6ed58ea08.wav b/tts/cache/9163f160a09185a3662f07d6ed58ea08.wav new file mode 100644 index 0000000..7677968 Binary files /dev/null and b/tts/cache/9163f160a09185a3662f07d6ed58ea08.wav differ diff --git a/tts/cache/91686540acd494dfcd3b53350e93cf13.wav b/tts/cache/91686540acd494dfcd3b53350e93cf13.wav new file mode 100644 index 0000000..c3da9e5 Binary files /dev/null and b/tts/cache/91686540acd494dfcd3b53350e93cf13.wav differ diff --git a/tts/cache/9296cce0966c88927300088cb1ca8f75.wav b/tts/cache/9296cce0966c88927300088cb1ca8f75.wav new file mode 100644 index 0000000..c35f759 Binary files /dev/null and b/tts/cache/9296cce0966c88927300088cb1ca8f75.wav differ diff --git a/tts/cache/92d8a00ce5f86cd4e2b76c043fded7b3.wav b/tts/cache/92d8a00ce5f86cd4e2b76c043fded7b3.wav new file mode 100644 index 0000000..0fb8b8d Binary files /dev/null and b/tts/cache/92d8a00ce5f86cd4e2b76c043fded7b3.wav differ diff --git a/tts/cache/93c60d916774276a82d27d13619cfcbd.wav b/tts/cache/93c60d916774276a82d27d13619cfcbd.wav new file mode 100644 index 0000000..9376385 Binary files /dev/null and b/tts/cache/93c60d916774276a82d27d13619cfcbd.wav differ diff --git a/tts/cache/93fabbce3b8eaab3ca3a2dade0b2388a.wav b/tts/cache/93fabbce3b8eaab3ca3a2dade0b2388a.wav new file mode 100644 index 0000000..178c80c Binary files /dev/null and b/tts/cache/93fabbce3b8eaab3ca3a2dade0b2388a.wav differ diff --git a/tts/cache/959e4ca427b86c41f67c4a8626d08299.wav b/tts/cache/959e4ca427b86c41f67c4a8626d08299.wav new file mode 100644 index 0000000..406e911 Binary files /dev/null and b/tts/cache/959e4ca427b86c41f67c4a8626d08299.wav differ diff --git a/tts/cache/965769bed41765022cabb27c26d9db12.wav b/tts/cache/965769bed41765022cabb27c26d9db12.wav new file mode 100644 index 0000000..6c3bb7f Binary files /dev/null and b/tts/cache/965769bed41765022cabb27c26d9db12.wav differ diff --git a/tts/cache/96b2dfd6e5e31ff7ca56ae65af0ba683.wav b/tts/cache/96b2dfd6e5e31ff7ca56ae65af0ba683.wav new file mode 100644 index 0000000..2b3a3a8 Binary files /dev/null and b/tts/cache/96b2dfd6e5e31ff7ca56ae65af0ba683.wav differ diff --git a/tts/cache/9720cb9c749c1dae161e476d4be977cf.wav b/tts/cache/9720cb9c749c1dae161e476d4be977cf.wav new file mode 100644 index 0000000..6ab7144 Binary files /dev/null and b/tts/cache/9720cb9c749c1dae161e476d4be977cf.wav differ diff --git a/tts/cache/973071324dd8ccbf6fccd1de7fb4a9b3.wav b/tts/cache/973071324dd8ccbf6fccd1de7fb4a9b3.wav new file mode 100644 index 0000000..6c7080a Binary files /dev/null and b/tts/cache/973071324dd8ccbf6fccd1de7fb4a9b3.wav differ diff --git a/tts/cache/975d78e9d23250418e5fd8a0ca54b1e9.txt b/tts/cache/975d78e9d23250418e5fd8a0ca54b1e9.txt new file mode 100644 index 0000000..e4008c6 --- /dev/null +++ b/tts/cache/975d78e9d23250418e5fd8a0ca54b1e9.txt @@ -0,0 +1 @@ +Willkommen bei Ihrem Ausflug ins historische Herz der Dominikanischen Republik. Seht euch das beeindruckende Alcázar de Colón in Santo Domingo an. Dieses prächtige Gebäude aus dem 15. Jahrhundert ist ein Zeugnis der Zeit, als die Brüder Columbus die Neuen Welt entdeckten. Die Architektur ist ein beeindruckendes Beispiel für die maurische Renaissance, mit ihren farbenfrohen Muster und eleganten Fenstern. Kulturell ist es von großer Bedeutung, da hier die Columbus-Familie lebte und sie den Durchmarsch in die Neuen Welt aus der Hauptstadt Santo Domingo auskundeten. Ein besonderer Anblick ist das 'Sala de los Descubrimientos', wo man die alten Weltkarten und persönliche Gegenstände der Familie Columbus bewundern kann. Interessante Fakten? Die Türen des Palastes wurden ursprünglich aus dem Palazzo Ducale in Venedig stammen. Besuchszeiten: Täglich von 9:00 bis 17:00. Geniesst euren Besuch in diesem historischen Juwel! \ No newline at end of file diff --git a/tts/cache/975d78e9d23250418e5fd8a0ca54b1e9.wav b/tts/cache/975d78e9d23250418e5fd8a0ca54b1e9.wav new file mode 100644 index 0000000..e69de29 diff --git a/tts/cache/97bec7b3f510e63927d207edc7606dc0.wav b/tts/cache/97bec7b3f510e63927d207edc7606dc0.wav new file mode 100644 index 0000000..f02b807 Binary files /dev/null and b/tts/cache/97bec7b3f510e63927d207edc7606dc0.wav differ diff --git a/tts/cache/981f19bcb91f369961279f6d50c8d0b2.wav b/tts/cache/981f19bcb91f369961279f6d50c8d0b2.wav new file mode 100644 index 0000000..02eab9a Binary files /dev/null and b/tts/cache/981f19bcb91f369961279f6d50c8d0b2.wav differ diff --git a/tts/cache/983356f9efc1f8f85f039e7333834832.wav b/tts/cache/983356f9efc1f8f85f039e7333834832.wav new file mode 100644 index 0000000..80b0e24 Binary files /dev/null and b/tts/cache/983356f9efc1f8f85f039e7333834832.wav differ diff --git a/tts/cache/984ba472f274c194d2efb39456dca4f4.wav b/tts/cache/984ba472f274c194d2efb39456dca4f4.wav new file mode 100644 index 0000000..d8e7d45 Binary files /dev/null and b/tts/cache/984ba472f274c194d2efb39456dca4f4.wav differ diff --git a/tts/cache/986c4cffd8a15c1e2992ced73ecb6027.wav b/tts/cache/986c4cffd8a15c1e2992ced73ecb6027.wav new file mode 100644 index 0000000..c57e41f Binary files /dev/null and b/tts/cache/986c4cffd8a15c1e2992ced73ecb6027.wav differ diff --git a/tts/cache/99a46ff1d78db8d91f08f939fbb5f9e4.wav b/tts/cache/99a46ff1d78db8d91f08f939fbb5f9e4.wav new file mode 100644 index 0000000..ac21a29 Binary files /dev/null and b/tts/cache/99a46ff1d78db8d91f08f939fbb5f9e4.wav differ diff --git a/tts/cache/9a6bac2baca082c064055cf707fe4c12.wav b/tts/cache/9a6bac2baca082c064055cf707fe4c12.wav new file mode 100644 index 0000000..df9f208 Binary files /dev/null and b/tts/cache/9a6bac2baca082c064055cf707fe4c12.wav differ diff --git a/tts/cache/9ac0f7320849d701be8d1c994b44d334.wav b/tts/cache/9ac0f7320849d701be8d1c994b44d334.wav new file mode 100644 index 0000000..575bbef Binary files /dev/null and b/tts/cache/9ac0f7320849d701be8d1c994b44d334.wav differ diff --git a/tts/cache/9b31964246544abc4ca0a8eb61d07bb8.wav b/tts/cache/9b31964246544abc4ca0a8eb61d07bb8.wav new file mode 100644 index 0000000..359eec9 Binary files /dev/null and b/tts/cache/9b31964246544abc4ca0a8eb61d07bb8.wav differ diff --git a/tts/cache/9b8eab636c72122444394649e8595581.wav b/tts/cache/9b8eab636c72122444394649e8595581.wav new file mode 100644 index 0000000..6ba1693 Binary files /dev/null and b/tts/cache/9b8eab636c72122444394649e8595581.wav differ diff --git a/tts/cache/9c415462a3f84ad27de0e6dc7575fed2.wav b/tts/cache/9c415462a3f84ad27de0e6dc7575fed2.wav new file mode 100644 index 0000000..f32eb04 Binary files /dev/null and b/tts/cache/9c415462a3f84ad27de0e6dc7575fed2.wav differ diff --git a/tts/cache/9cee8013963dcf928380d4b5f476e1e6.wav b/tts/cache/9cee8013963dcf928380d4b5f476e1e6.wav new file mode 100644 index 0000000..648f29a Binary files /dev/null and b/tts/cache/9cee8013963dcf928380d4b5f476e1e6.wav differ diff --git a/tts/cache/9d72fe36e2e50e0d36bae205fc240460.wav b/tts/cache/9d72fe36e2e50e0d36bae205fc240460.wav new file mode 100644 index 0000000..9326587 Binary files /dev/null and b/tts/cache/9d72fe36e2e50e0d36bae205fc240460.wav differ diff --git a/tts/cache/9f3b1deec68990f434a3398b69cc4495.wav b/tts/cache/9f3b1deec68990f434a3398b69cc4495.wav new file mode 100644 index 0000000..c5ef13e Binary files /dev/null and b/tts/cache/9f3b1deec68990f434a3398b69cc4495.wav differ diff --git a/tts/cache/9f946e096157e41d47bd554d3817e8b0.wav b/tts/cache/9f946e096157e41d47bd554d3817e8b0.wav new file mode 100644 index 0000000..93703c5 Binary files /dev/null and b/tts/cache/9f946e096157e41d47bd554d3817e8b0.wav differ diff --git a/tts/cache/9fa1195f5384d1ec634428c724ef4a29.wav b/tts/cache/9fa1195f5384d1ec634428c724ef4a29.wav new file mode 100644 index 0000000..13915fd Binary files /dev/null and b/tts/cache/9fa1195f5384d1ec634428c724ef4a29.wav differ diff --git a/tts/cache/9fe13d235536947d2500460db760dc0a.wav b/tts/cache/9fe13d235536947d2500460db760dc0a.wav new file mode 100644 index 0000000..86d4bfc Binary files /dev/null and b/tts/cache/9fe13d235536947d2500460db760dc0a.wav differ diff --git a/tts/cache/a09291d58b11bbf3c03499f2e5493d9b.wav b/tts/cache/a09291d58b11bbf3c03499f2e5493d9b.wav new file mode 100644 index 0000000..05b5d94 Binary files /dev/null and b/tts/cache/a09291d58b11bbf3c03499f2e5493d9b.wav differ diff --git a/tts/cache/a134200ab8f60b49cbd9e73034607234.wav b/tts/cache/a134200ab8f60b49cbd9e73034607234.wav new file mode 100644 index 0000000..2594e7d Binary files /dev/null and b/tts/cache/a134200ab8f60b49cbd9e73034607234.wav differ diff --git a/tts/cache/a13fca41cb43473b373dea9f3c91289f.wav b/tts/cache/a13fca41cb43473b373dea9f3c91289f.wav new file mode 100644 index 0000000..50356db Binary files /dev/null and b/tts/cache/a13fca41cb43473b373dea9f3c91289f.wav differ diff --git a/tts/cache/a159df0d648ff82dbf963e9ceea1870e.wav b/tts/cache/a159df0d648ff82dbf963e9ceea1870e.wav new file mode 100644 index 0000000..882a169 Binary files /dev/null and b/tts/cache/a159df0d648ff82dbf963e9ceea1870e.wav differ diff --git a/tts/cache/a3cd5a6b49a71492581e34003e63f363.wav b/tts/cache/a3cd5a6b49a71492581e34003e63f363.wav new file mode 100644 index 0000000..ccf023f Binary files /dev/null and b/tts/cache/a3cd5a6b49a71492581e34003e63f363.wav differ diff --git a/tts/cache/a3e22d65a1e096d63bb63e7b53674f09.wav b/tts/cache/a3e22d65a1e096d63bb63e7b53674f09.wav new file mode 100644 index 0000000..4c7b9b9 Binary files /dev/null and b/tts/cache/a3e22d65a1e096d63bb63e7b53674f09.wav differ diff --git a/tts/cache/a3fb66089333febc55a5511a805ceb82.wav b/tts/cache/a3fb66089333febc55a5511a805ceb82.wav new file mode 100644 index 0000000..770ff2f Binary files /dev/null and b/tts/cache/a3fb66089333febc55a5511a805ceb82.wav differ diff --git a/tts/cache/a48d6fafbffdbe4d5f800b78714532f9.wav b/tts/cache/a48d6fafbffdbe4d5f800b78714532f9.wav new file mode 100644 index 0000000..9fdf856 Binary files /dev/null and b/tts/cache/a48d6fafbffdbe4d5f800b78714532f9.wav differ diff --git a/tts/cache/a578f8cb025e666699bceb85a5ccdb48.wav b/tts/cache/a578f8cb025e666699bceb85a5ccdb48.wav new file mode 100644 index 0000000..9e717e1 Binary files /dev/null and b/tts/cache/a578f8cb025e666699bceb85a5ccdb48.wav differ diff --git a/tts/cache/a5e07c1d098afe8cd8e71cb22bdf8ae2.wav b/tts/cache/a5e07c1d098afe8cd8e71cb22bdf8ae2.wav new file mode 100644 index 0000000..cb65620 Binary files /dev/null and b/tts/cache/a5e07c1d098afe8cd8e71cb22bdf8ae2.wav differ diff --git a/tts/cache/a5eeef94484ed51a76386dc5f79fb636.wav b/tts/cache/a5eeef94484ed51a76386dc5f79fb636.wav new file mode 100644 index 0000000..47340e7 Binary files /dev/null and b/tts/cache/a5eeef94484ed51a76386dc5f79fb636.wav differ diff --git a/tts/cache/a6236da75e96e6181662b045105119fe.wav b/tts/cache/a6236da75e96e6181662b045105119fe.wav new file mode 100644 index 0000000..1ac5ba5 Binary files /dev/null and b/tts/cache/a6236da75e96e6181662b045105119fe.wav differ diff --git a/tts/cache/a6bf0024a23c252e4cbaa8f25c16d853.wav b/tts/cache/a6bf0024a23c252e4cbaa8f25c16d853.wav new file mode 100644 index 0000000..8309061 Binary files /dev/null and b/tts/cache/a6bf0024a23c252e4cbaa8f25c16d853.wav differ diff --git a/tts/cache/a754bfa5e66c6558e7c9bb901d7d1c69.wav b/tts/cache/a754bfa5e66c6558e7c9bb901d7d1c69.wav new file mode 100644 index 0000000..98bbe15 Binary files /dev/null and b/tts/cache/a754bfa5e66c6558e7c9bb901d7d1c69.wav differ diff --git a/tts/cache/a76d0f8cdc14a93d45f75b9f7308301c.wav b/tts/cache/a76d0f8cdc14a93d45f75b9f7308301c.wav new file mode 100644 index 0000000..a337294 Binary files /dev/null and b/tts/cache/a76d0f8cdc14a93d45f75b9f7308301c.wav differ diff --git a/tts/cache/a886e732a045efd4e6ed003eb5ddf631.wav b/tts/cache/a886e732a045efd4e6ed003eb5ddf631.wav new file mode 100644 index 0000000..7078b2e Binary files /dev/null and b/tts/cache/a886e732a045efd4e6ed003eb5ddf631.wav differ diff --git a/tts/cache/a99c3a6c4a3f0f02ef23159b5911c6f9.wav b/tts/cache/a99c3a6c4a3f0f02ef23159b5911c6f9.wav new file mode 100644 index 0000000..e2b69a5 Binary files /dev/null and b/tts/cache/a99c3a6c4a3f0f02ef23159b5911c6f9.wav differ diff --git a/tts/cache/aa3e1b0b79b272405f715787d6ba17ab.wav b/tts/cache/aa3e1b0b79b272405f715787d6ba17ab.wav new file mode 100644 index 0000000..d72add7 Binary files /dev/null and b/tts/cache/aa3e1b0b79b272405f715787d6ba17ab.wav differ diff --git a/tts/cache/aa6292cc786cf3e3aa288a8100a06dbc.wav b/tts/cache/aa6292cc786cf3e3aa288a8100a06dbc.wav new file mode 100644 index 0000000..42b1395 Binary files /dev/null and b/tts/cache/aa6292cc786cf3e3aa288a8100a06dbc.wav differ diff --git a/tts/cache/aaa68c23891cb59832bf6ad0ec77ad87.wav b/tts/cache/aaa68c23891cb59832bf6ad0ec77ad87.wav new file mode 100644 index 0000000..84b719a Binary files /dev/null and b/tts/cache/aaa68c23891cb59832bf6ad0ec77ad87.wav differ diff --git a/tts/cache/abcdc02e8a761a93198b952a57856b45.wav b/tts/cache/abcdc02e8a761a93198b952a57856b45.wav new file mode 100644 index 0000000..8d4b0bf Binary files /dev/null and b/tts/cache/abcdc02e8a761a93198b952a57856b45.wav differ diff --git a/tts/cache/abe56f70e60822337845df322651eb56.wav b/tts/cache/abe56f70e60822337845df322651eb56.wav new file mode 100644 index 0000000..c1932d8 Binary files /dev/null and b/tts/cache/abe56f70e60822337845df322651eb56.wav differ diff --git a/tts/cache/ac77578105f7afefc33ea0192c0d86a5.wav b/tts/cache/ac77578105f7afefc33ea0192c0d86a5.wav new file mode 100644 index 0000000..451b408 Binary files /dev/null and b/tts/cache/ac77578105f7afefc33ea0192c0d86a5.wav differ diff --git a/tts/cache/ad12f6aaf2a871d5534e69ed105cde60.wav b/tts/cache/ad12f6aaf2a871d5534e69ed105cde60.wav new file mode 100644 index 0000000..df74661 Binary files /dev/null and b/tts/cache/ad12f6aaf2a871d5534e69ed105cde60.wav differ diff --git a/tts/cache/addbf35047281eb921982e0dae6755af.wav b/tts/cache/addbf35047281eb921982e0dae6755af.wav new file mode 100644 index 0000000..1215abd Binary files /dev/null and b/tts/cache/addbf35047281eb921982e0dae6755af.wav differ diff --git a/tts/cache/ae626976b68660dc469a3118fe56f202.wav b/tts/cache/ae626976b68660dc469a3118fe56f202.wav new file mode 100644 index 0000000..a9a0843 Binary files /dev/null and b/tts/cache/ae626976b68660dc469a3118fe56f202.wav differ diff --git a/tts/cache/aeec9cc1fcfadc1dac8434df88c31cd9.wav b/tts/cache/aeec9cc1fcfadc1dac8434df88c31cd9.wav new file mode 100644 index 0000000..b43acc4 Binary files /dev/null and b/tts/cache/aeec9cc1fcfadc1dac8434df88c31cd9.wav differ diff --git a/tts/cache/af22563f58d5c4888874514d5d12794c.wav b/tts/cache/af22563f58d5c4888874514d5d12794c.wav new file mode 100644 index 0000000..4ad8ca9 Binary files /dev/null and b/tts/cache/af22563f58d5c4888874514d5d12794c.wav differ diff --git a/tts/cache/audio_guides.json b/tts/cache/audio_guides.json new file mode 100644 index 0000000..d2ada84 --- /dev/null +++ b/tts/cache/audio_guides.json @@ -0,0 +1,58 @@ +{ + "7a0b9632873dbba306c7a8f743ebde3f": { + "audioUrl": "https://api.karibeo.ai:8443/api/v1/tts/audio/5e3460083948f1bf86da7378f45469a3.wav", + "transcript": "¡Hola! Bienvenidos a la Catedral Primada de América, el más antiguo templo católico de las Américas. 🏛️ Estamos en Santo Domingo, la capital de la República Dominicana. \n\nConstruida en 1512 por los españoles, esta iglesia majestuosa ha visto la historia en la isla. 🕰️ La Catedral es famosa por ser el primer edificio de la Nueva World construido con finalidades religiosas y civiles. \n\nDato curioso: ¿Sabías que en la Catedral Primada se encuentra el sepulcro de Cristóbal Colón? 🤔 Sí, el descubridor de América. \n\nAdmira su arquitecturagótica, sus capillas y la belleza de sus vidrieras. 🏰🌟 Recuerda que cada detalle cuenta, cada piedra tiene una historia. \n\nDisfruta de esta visita y siente el peso de la historia en cada paso. ¡Que tengas una experiencia inolvidable! 🌿🌈", + "language": "es", + "placeName": "Catedral Primada de América", + "createdAt": 1773608773865 + }, + "c289dbe5a50ae27ff33bff513f6f2d1b": { + "audioUrl": "https://api.karibeo.ai:8443/api/v1/tts/audio/2b2796df536e031bf44600ebd65b963d.wav", + "transcript": "¡Hola! Bienvenido a Santo Domingo, la capital de la cultura dominicana. 🏛️ Estamos frente al Alcázar de Colón, un monumento icónico que data del siglo XV. 🕰️\n\nImagina que en 1510, Diego, el hijo de Cristóbal Colón, comenzó a construir este impresionante palacio en honor a su padre. 👑 Fue el primer edificio europeo en América. 🌏\n\nCaminamos por sus calles adoquinadas y notamos su arquitectura gótica, un reflejo de la nobleza de la época. 🏰\n\nAhora, un dato curioso: ¿Sabías que el Alcázar de Colón está considerado el primer museo de América? 🤓 En sus salones, encontrarás piezas de arte y objetos de la época colonial, un viaje a través del tiempo. 🖼️\n\nDisfruta de la visita, toma fotos y siente la historia en cada piedra de este lugar mágico. ¡Que tu experiencia en el Alcázar de Colón sea inolvidable! 📸💖", + "language": "es", + "placeName": "Alcázar de Colón", + "createdAt": 1773609167784 + }, + "cb932443c46f581243c2914c6ba3e38e": { + "audioUrl": "https://api.karibeo.ai:8443/api/v1/tts/audio/79609a8254bec058c2d04bcf2cfe3916.wav", + "transcript": "¡Hola viajero! 🏰 Te doy la bienvenida a la impresionante Fortaleza Ozama en Santo Domingo. ⚔️ Esta fortaleza fue construida en el siglo XVI y es una joya de la arquitectura renacentista en América. 🌟\n\nCaminemos juntos por sus muros, donde una vez resonaban los pasos de soldados españoles. 🕰️ La Fortaleza Ozama fue diseñada por el mismo arquitecto que creó el Palacio de Bellas Artes en México, Diego Portes. 👀\n\nAh, y un dato curioso: ¿sabes que fue la última fortaleza en caer durante el asedio británico de 1586? 🏰💥\n\nExplora sus calles empedradas, admira las vistas panorámicas de la bahía y siente la historia en el aire. 🌅 Esta es una experiencia que no te olvidarás. ¡Disfruta de tu visita a este símbolo de la resistencia dominicana! 🇩🇴💖", + "language": "es", + "placeName": "Fortaleza Ozama", + "createdAt": 1773609383293 + }, + "aadcae8002444c13704eee9a48339217": { + "audioUrl": "https://api.karibeo.ai:8443/api/v1/tts/audio/b4608e0b75e08a2ab13f52f8e8ea570b.wav", + "transcript": "Benvenuti nella Cattedrale Primaziale delle Americhe 🏛️, un capolavoro architettonico a Santo Domingo! Immaginate, questa cattedrale fu dedicata nel 1540, rendendola la più antica del Nuovo Mondo. 🌍\n\n\"Ma perché è così importante?\", vi chiederete. Ecco, è stata progettata da un genio dell'epoca, il quale scelse di costruirla con un misto di stili: gotico, renascimento e plateresco. 🏰\n\nCuriosità: Sotto la cattedrale, c'è una cripta che ospita le spoglie di personalità storiche importanti. 💀\n\nOra, lasciatemi guidare nella scoperta di questa gioia architettonica, dove ogni angolo racconta storie di eroi, esploratori e leggende. 📜\n\nGira e goditi questa testimonianza di un'epoca che ha scolpito la storia delle Americhe. 🌟", + "language": "it", + "placeName": "Cattedrale Primaziale delle Americhe", + "createdAt": 1773609794301 + }, + "32a8eb600f0ab28258358d07019c3523": { + "audioUrl": "https://api.karibeo.ai:8443/api/v1/tts/audio/140d602c3941489184eb88df1ed25f52.wav", + "transcript": "Benvenuti nel cuore della Repubblica Dominicana, dove l'antico splendore si incontra con la modernità! 🏰 Oggi vi accompagneremo attraverso i secoli con una visita al majestuoso Alcázar de Colón, il palazzo storico di Santo Domingo. 🕰️\n\n\"Ecco l'Alcázar de Colón, costruito nel 1510 dal figlio di Cristoforo Colombo, Diego, e sua moglie Maria. Questo edificio è uno dei pochi esempi di architettura gotica nella Repubblica Dominicana e racchiude storie di avventure e scoperte. 🗺️\n\nCuriosità: Non sapete mai? Questo palazzo ospitò anche una volta il governatore della colonia spagnola. Oggi è un museo che mostra preziosi manufatti e dipinti, testimoni del periodo coloniale. 🖼️\n\nVi aspettano stanze affrescate e una vista mozzafiato sulla baia di Santo Domingo. Immergiti nel passato e vivi l'essenza dell'antica Spagna. 🌅\n\nGli amanti dell'arte e della storia troveranno qui il loro paradiso. 🎨 E non dimenticate la vostra foto all'ingresso, è un'occasione unica! 📸\"", + "language": "it", + "placeName": "Alcázar de Colón", + "createdAt": 1773610114988 + }, + "780292b4caa2277120ae264f968c5df2": { + "audioUrl": "https://api.karibeo.ai:8443/api/v1/tts/audio/6130b4142474f3b16b08a74301fad447.wav", + "transcript": "¡Hola! Bienvenido a este audio tour único al Faro a Colón en la República Dominicana. 🎧🚢\n\nImagina, tú estás frente a una maravilla arquitectónica de 68 metros de altura, un símbolo de amistad entre España y República Dominicana. Construido en 1992, este faro fue dedicado al descubridor Cristóbal Colón y al pueblo dominicano. 🗼🌟\n\nCaminando por sus alrededores, te sumergirás en la historia de la expedición de Colón, mientras disfrutas de las vistas panorámicas de la bahía de Santo Domingo. 🌅🏞️\n\nPero espera, hay un dato curioso: ¡Este faro contiene las cenizas del propio Cristóbal Colón! 🔥🪦 Así que, de alguna manera, estás en contacto con la historia misma. \n\nDisfruta de tu visita, toma fotos y reflexiona en la importancia de este lugar en la historia mundial. ¡Que tu experiencia sea memorable! 📸💖", + "language": "es", + "placeName": "Faro a Colón", + "createdAt": 1773700774549 + }, + "b5c05b215d94cf386df0342a050b14b2": { + "audioUrl": "https://api.karibeo.ai:8443/api/v1/tts/audio/e090e9952ae2a455a5bb60416c918e62.wav", + "transcript": "¡Hola viajero! 🏛️ Te damos la bienvenida al Museo de Larimar en la República Dominicana, un lugar único en el mundo. 🌍\n\nImaginarás, la Larimar, nuestra joya nacional, nace de las olas del Caribe. 🌊💎 Esta piedra azul única fue descubierta aquí en 1974 por un minero y su hija, que al ver su color, comparó con el mar caribeño. 🌊\n\nAhora, te llevaré de la mano por este museo donde cada exhibición cuenta nuestra historia. 👋📜\n\n¿Sabías que la Larimar solo se encuentra en la isla豆a Beata, a 150 km de aquí? 🏝️ Es un dato curioso, ¿verdad? 🤓\n\nPasa cada pieza, destellos de azul en cada rincón. 🔵💠 Aprovecha para tocar, sentir y entender la conexión entre nuestro mar y estas joyas. 🌊💎\n\n¡Disfruta de este paseo por la belleza única de la Larimar! 🥰", + "language": "es", + "placeName": "Museo de Larimar", + "createdAt": 1773703600597 + }, + "8b35367434fa515d739fa731098f39c9": { + "audioUrl": "https://api.karibeo.ai:8443/api/v1/tts/audio/3c913aa4eba84bc2996d9b77de30841a.wav", + "transcript": "¡Hola! 🏛️ Bienvenido a la Casa de Tostado en República Dominicana. Imagínate, en el siglo XVI, este era el hogar de uno de los primeros comerciantes españoles, Nicolás de Tostado. 🏠\n\nCaminando por sus calles adoquinadas, puedes sentir el aroma de la historia. 🚶‍♂️🕰️ La Casa de Tostado es un testimonio de la arquitectura colonial, con techos de vigas y techumbres de caneyes. 🏡\n\nCuriosamente, este edificio fue construido sobre una antigua aldea taino, y hay evidencias de su presencia en los muros, que aún conservan inscripciones indígenas. 🤓\n\nDisfruta de la visita, y recuerda que cada piedra aquí tiene una historia que contar. ¡Que tu visita sea un viaje a través del tiempo en la bella República Dominicana! 🌴🌟", + "language": "es", + "placeName": "Casa de Tostado", + "createdAt": 1773703948007 + } +} \ No newline at end of file diff --git a/tts/cache/b09e7933a9d6d90f35b3914f55abc088.wav b/tts/cache/b09e7933a9d6d90f35b3914f55abc088.wav new file mode 100644 index 0000000..8a0fff8 Binary files /dev/null and b/tts/cache/b09e7933a9d6d90f35b3914f55abc088.wav differ diff --git a/tts/cache/b0a74fd9df3c693c7175e67087f5d55f.wav b/tts/cache/b0a74fd9df3c693c7175e67087f5d55f.wav new file mode 100644 index 0000000..a4eb465 Binary files /dev/null and b/tts/cache/b0a74fd9df3c693c7175e67087f5d55f.wav differ diff --git a/tts/cache/b1c8ae00664f85c02ea70a1458e83870.wav b/tts/cache/b1c8ae00664f85c02ea70a1458e83870.wav new file mode 100644 index 0000000..7fa7d8e Binary files /dev/null and b/tts/cache/b1c8ae00664f85c02ea70a1458e83870.wav differ diff --git a/tts/cache/b25e4dc587d8a30ffb0ec3505016a476.wav b/tts/cache/b25e4dc587d8a30ffb0ec3505016a476.wav new file mode 100644 index 0000000..f9ead08 Binary files /dev/null and b/tts/cache/b25e4dc587d8a30ffb0ec3505016a476.wav differ diff --git a/tts/cache/b2958dacf8d19074afc8821969dd13f1.wav b/tts/cache/b2958dacf8d19074afc8821969dd13f1.wav new file mode 100644 index 0000000..a9a5b69 Binary files /dev/null and b/tts/cache/b2958dacf8d19074afc8821969dd13f1.wav differ diff --git a/tts/cache/b38e162ce7b8fbd0c22ef490335059d3.wav b/tts/cache/b38e162ce7b8fbd0c22ef490335059d3.wav new file mode 100644 index 0000000..70026ed Binary files /dev/null and b/tts/cache/b38e162ce7b8fbd0c22ef490335059d3.wav differ diff --git a/tts/cache/b40a722395572b9990158fb440c6fa37.wav b/tts/cache/b40a722395572b9990158fb440c6fa37.wav new file mode 100644 index 0000000..227dd6a Binary files /dev/null and b/tts/cache/b40a722395572b9990158fb440c6fa37.wav differ diff --git a/tts/cache/b451c4222ad520f940c0479e9772ae82.wav b/tts/cache/b451c4222ad520f940c0479e9772ae82.wav new file mode 100644 index 0000000..c3c7a3a Binary files /dev/null and b/tts/cache/b451c4222ad520f940c0479e9772ae82.wav differ diff --git a/tts/cache/b4608e0b75e08a2ab13f52f8e8ea570b.wav b/tts/cache/b4608e0b75e08a2ab13f52f8e8ea570b.wav new file mode 100644 index 0000000..05c9f67 Binary files /dev/null and b/tts/cache/b4608e0b75e08a2ab13f52f8e8ea570b.wav differ diff --git a/tts/cache/b53fef88e74f5edad4336cabdf8c9c49.wav b/tts/cache/b53fef88e74f5edad4336cabdf8c9c49.wav new file mode 100644 index 0000000..ea4e005 Binary files /dev/null and b/tts/cache/b53fef88e74f5edad4336cabdf8c9c49.wav differ diff --git a/tts/cache/b5a55b69b524809c0af0e0b02970b348.wav b/tts/cache/b5a55b69b524809c0af0e0b02970b348.wav new file mode 100644 index 0000000..ae473d7 Binary files /dev/null and b/tts/cache/b5a55b69b524809c0af0e0b02970b348.wav differ diff --git a/tts/cache/b789d777c669d0b4c31253d8a4168452.wav b/tts/cache/b789d777c669d0b4c31253d8a4168452.wav new file mode 100644 index 0000000..93ea5c7 Binary files /dev/null and b/tts/cache/b789d777c669d0b4c31253d8a4168452.wav differ diff --git a/tts/cache/b8d0ad3fc20d3cd13ba4aa16870af303.wav b/tts/cache/b8d0ad3fc20d3cd13ba4aa16870af303.wav new file mode 100644 index 0000000..7c677f4 Binary files /dev/null and b/tts/cache/b8d0ad3fc20d3cd13ba4aa16870af303.wav differ diff --git a/tts/cache/b93ba841a1fdb277d7b69eed61ddad47.wav b/tts/cache/b93ba841a1fdb277d7b69eed61ddad47.wav new file mode 100644 index 0000000..0130cb5 Binary files /dev/null and b/tts/cache/b93ba841a1fdb277d7b69eed61ddad47.wav differ diff --git a/tts/cache/b9408860b6c904097e70d811125bb5df.wav b/tts/cache/b9408860b6c904097e70d811125bb5df.wav new file mode 100644 index 0000000..9d32e18 Binary files /dev/null and b/tts/cache/b9408860b6c904097e70d811125bb5df.wav differ diff --git a/tts/cache/ba0cd8798b17204a2ce541bf73c92b7b.wav b/tts/cache/ba0cd8798b17204a2ce541bf73c92b7b.wav new file mode 100644 index 0000000..3f7290a Binary files /dev/null and b/tts/cache/ba0cd8798b17204a2ce541bf73c92b7b.wav differ diff --git a/tts/cache/ba567515e681d8a4373c7e5339f7da72.wav b/tts/cache/ba567515e681d8a4373c7e5339f7da72.wav new file mode 100644 index 0000000..30925dd Binary files /dev/null and b/tts/cache/ba567515e681d8a4373c7e5339f7da72.wav differ diff --git a/tts/cache/ba7a8eae88bc47a46cb58ad04f03a1b5.wav b/tts/cache/ba7a8eae88bc47a46cb58ad04f03a1b5.wav new file mode 100644 index 0000000..5881315 Binary files /dev/null and b/tts/cache/ba7a8eae88bc47a46cb58ad04f03a1b5.wav differ diff --git a/tts/cache/ba9a86a0ef03c9c4bd64ad06df94afbd.wav b/tts/cache/ba9a86a0ef03c9c4bd64ad06df94afbd.wav new file mode 100644 index 0000000..66f8ad7 Binary files /dev/null and b/tts/cache/ba9a86a0ef03c9c4bd64ad06df94afbd.wav differ diff --git a/tts/cache/baf480d21c9b206b223349db61a0c24d.wav b/tts/cache/baf480d21c9b206b223349db61a0c24d.wav new file mode 100644 index 0000000..042c9b2 Binary files /dev/null and b/tts/cache/baf480d21c9b206b223349db61a0c24d.wav differ diff --git a/tts/cache/bc1b1cf18f30676af1a411b6b4a5002a.wav b/tts/cache/bc1b1cf18f30676af1a411b6b4a5002a.wav new file mode 100644 index 0000000..468baf1 Binary files /dev/null and b/tts/cache/bc1b1cf18f30676af1a411b6b4a5002a.wav differ diff --git a/tts/cache/bc6cb2c29ced83b45452dc8e92465d37.wav b/tts/cache/bc6cb2c29ced83b45452dc8e92465d37.wav new file mode 100644 index 0000000..fc4c37f Binary files /dev/null and b/tts/cache/bc6cb2c29ced83b45452dc8e92465d37.wav differ diff --git a/tts/cache/bc77e22958aa4d07cd62a70ca73160ef.wav b/tts/cache/bc77e22958aa4d07cd62a70ca73160ef.wav new file mode 100644 index 0000000..1958672 Binary files /dev/null and b/tts/cache/bc77e22958aa4d07cd62a70ca73160ef.wav differ diff --git a/tts/cache/bc960b951d4ee87a14eafdb6318b9528.wav b/tts/cache/bc960b951d4ee87a14eafdb6318b9528.wav new file mode 100644 index 0000000..fc93f8e Binary files /dev/null and b/tts/cache/bc960b951d4ee87a14eafdb6318b9528.wav differ diff --git a/tts/cache/bd6ed1ba0d074997e83f391164a26130.wav b/tts/cache/bd6ed1ba0d074997e83f391164a26130.wav new file mode 100644 index 0000000..f6be28e Binary files /dev/null and b/tts/cache/bd6ed1ba0d074997e83f391164a26130.wav differ diff --git a/tts/cache/bde0146b59756d5a9b1816d5c8094f95.wav b/tts/cache/bde0146b59756d5a9b1816d5c8094f95.wav new file mode 100644 index 0000000..8b49c69 Binary files /dev/null and b/tts/cache/bde0146b59756d5a9b1816d5c8094f95.wav differ diff --git a/tts/cache/be2ce056fb6758b2bf3d9977fbe7a2d5.wav b/tts/cache/be2ce056fb6758b2bf3d9977fbe7a2d5.wav new file mode 100644 index 0000000..3e67f94 Binary files /dev/null and b/tts/cache/be2ce056fb6758b2bf3d9977fbe7a2d5.wav differ diff --git a/tts/cache/be33888e2e4e6921364854ff6c06f27d.wav b/tts/cache/be33888e2e4e6921364854ff6c06f27d.wav new file mode 100644 index 0000000..40f3293 Binary files /dev/null and b/tts/cache/be33888e2e4e6921364854ff6c06f27d.wav differ diff --git a/tts/cache/be64a1eee52d8938469cccffabe7c439.wav b/tts/cache/be64a1eee52d8938469cccffabe7c439.wav new file mode 100644 index 0000000..eabbf2b Binary files /dev/null and b/tts/cache/be64a1eee52d8938469cccffabe7c439.wav differ diff --git a/tts/cache/be85b55bd88400e3dc58478811ffd34c.wav b/tts/cache/be85b55bd88400e3dc58478811ffd34c.wav new file mode 100644 index 0000000..5be1ff4 Binary files /dev/null and b/tts/cache/be85b55bd88400e3dc58478811ffd34c.wav differ diff --git a/tts/cache/bf0de14af741707a838c6f93ce0767cb.wav b/tts/cache/bf0de14af741707a838c6f93ce0767cb.wav new file mode 100644 index 0000000..3ed66fa Binary files /dev/null and b/tts/cache/bf0de14af741707a838c6f93ce0767cb.wav differ diff --git a/tts/cache/bf365ee0ac1d947b6546fcfec1a9b6f5.wav b/tts/cache/bf365ee0ac1d947b6546fcfec1a9b6f5.wav new file mode 100644 index 0000000..22f9385 Binary files /dev/null and b/tts/cache/bf365ee0ac1d947b6546fcfec1a9b6f5.wav differ diff --git a/tts/cache/bfe5365fc5b0aa0f5211d2229d2bf295.wav b/tts/cache/bfe5365fc5b0aa0f5211d2229d2bf295.wav new file mode 100644 index 0000000..dc7f4b5 Binary files /dev/null and b/tts/cache/bfe5365fc5b0aa0f5211d2229d2bf295.wav differ diff --git a/tts/cache/c003b3fabbcdcae7409081aaa79a705f.wav b/tts/cache/c003b3fabbcdcae7409081aaa79a705f.wav new file mode 100644 index 0000000..0039710 Binary files /dev/null and b/tts/cache/c003b3fabbcdcae7409081aaa79a705f.wav differ diff --git a/tts/cache/c1b705d2d6c85f7ff4c93e2dd42cdd80.wav b/tts/cache/c1b705d2d6c85f7ff4c93e2dd42cdd80.wav new file mode 100644 index 0000000..2f003eb Binary files /dev/null and b/tts/cache/c1b705d2d6c85f7ff4c93e2dd42cdd80.wav differ diff --git a/tts/cache/c249a7a7c13fc071c19d9ef2111e92a3.wav b/tts/cache/c249a7a7c13fc071c19d9ef2111e92a3.wav new file mode 100644 index 0000000..86b9abd Binary files /dev/null and b/tts/cache/c249a7a7c13fc071c19d9ef2111e92a3.wav differ diff --git a/tts/cache/c3d67828356e6b73b953fed82ed2f9d7.wav b/tts/cache/c3d67828356e6b73b953fed82ed2f9d7.wav new file mode 100644 index 0000000..ef79f98 Binary files /dev/null and b/tts/cache/c3d67828356e6b73b953fed82ed2f9d7.wav differ diff --git a/tts/cache/c430ea028a263bef227bd2fa2473c41a.wav b/tts/cache/c430ea028a263bef227bd2fa2473c41a.wav new file mode 100644 index 0000000..dc4dd2b Binary files /dev/null and b/tts/cache/c430ea028a263bef227bd2fa2473c41a.wav differ diff --git a/tts/cache/c4cda46a91cd8adecb022cee63989c6e.wav b/tts/cache/c4cda46a91cd8adecb022cee63989c6e.wav new file mode 100644 index 0000000..fe586fe Binary files /dev/null and b/tts/cache/c4cda46a91cd8adecb022cee63989c6e.wav differ diff --git a/tts/cache/c56ca508853378e15ce8fcbd4fe7a8a0.wav b/tts/cache/c56ca508853378e15ce8fcbd4fe7a8a0.wav new file mode 100644 index 0000000..08162c1 Binary files /dev/null and b/tts/cache/c56ca508853378e15ce8fcbd4fe7a8a0.wav differ diff --git a/tts/cache/c5811ea96f0d14884bb55bb5942382a8.wav b/tts/cache/c5811ea96f0d14884bb55bb5942382a8.wav new file mode 100644 index 0000000..f49c2be Binary files /dev/null and b/tts/cache/c5811ea96f0d14884bb55bb5942382a8.wav differ diff --git a/tts/cache/c5efff87e8c409ce519e8d22c6dff3b3.wav b/tts/cache/c5efff87e8c409ce519e8d22c6dff3b3.wav new file mode 100644 index 0000000..d27c4c5 Binary files /dev/null and b/tts/cache/c5efff87e8c409ce519e8d22c6dff3b3.wav differ diff --git a/tts/cache/c76cd134367074596c4a4d10bd666ba4.wav b/tts/cache/c76cd134367074596c4a4d10bd666ba4.wav new file mode 100644 index 0000000..f67adb1 Binary files /dev/null and b/tts/cache/c76cd134367074596c4a4d10bd666ba4.wav differ diff --git a/tts/cache/c7b7d2b12dfeb800b7bb62222a6be5ce.wav b/tts/cache/c7b7d2b12dfeb800b7bb62222a6be5ce.wav new file mode 100644 index 0000000..beb3e10 Binary files /dev/null and b/tts/cache/c7b7d2b12dfeb800b7bb62222a6be5ce.wav differ diff --git a/tts/cache/c7d4438a6ce02c5e329a32638ba50f61.wav b/tts/cache/c7d4438a6ce02c5e329a32638ba50f61.wav new file mode 100644 index 0000000..0fcf445 Binary files /dev/null and b/tts/cache/c7d4438a6ce02c5e329a32638ba50f61.wav differ diff --git a/tts/cache/c8786e66a92a70e6e6c4632fdcbab070.wav b/tts/cache/c8786e66a92a70e6e6c4632fdcbab070.wav new file mode 100644 index 0000000..10c9cdb Binary files /dev/null and b/tts/cache/c8786e66a92a70e6e6c4632fdcbab070.wav differ diff --git a/tts/cache/c95680ced557d08c3059cc73983028bb.wav b/tts/cache/c95680ced557d08c3059cc73983028bb.wav new file mode 100644 index 0000000..ebee77e Binary files /dev/null and b/tts/cache/c95680ced557d08c3059cc73983028bb.wav differ diff --git a/tts/cache/c9874ae7c0006192ce18cd2edbccceee.wav b/tts/cache/c9874ae7c0006192ce18cd2edbccceee.wav new file mode 100644 index 0000000..706c4ff Binary files /dev/null and b/tts/cache/c9874ae7c0006192ce18cd2edbccceee.wav differ diff --git a/tts/cache/ca3b33841dd118bb7c1c9ba29fd3819c.wav b/tts/cache/ca3b33841dd118bb7c1c9ba29fd3819c.wav new file mode 100644 index 0000000..6058cc1 Binary files /dev/null and b/tts/cache/ca3b33841dd118bb7c1c9ba29fd3819c.wav differ diff --git a/tts/cache/ca71455c046c9a5bc6f0fd3a1d9387c5.wav b/tts/cache/ca71455c046c9a5bc6f0fd3a1d9387c5.wav new file mode 100644 index 0000000..2d5ab6a Binary files /dev/null and b/tts/cache/ca71455c046c9a5bc6f0fd3a1d9387c5.wav differ diff --git a/tts/cache/cacc95b7d432a6c5bc83631fa08d32ab.wav b/tts/cache/cacc95b7d432a6c5bc83631fa08d32ab.wav new file mode 100644 index 0000000..8c85a40 Binary files /dev/null and b/tts/cache/cacc95b7d432a6c5bc83631fa08d32ab.wav differ diff --git a/tts/cache/cafeca58c121acabcd6a1a8ceae958f8.wav b/tts/cache/cafeca58c121acabcd6a1a8ceae958f8.wav new file mode 100644 index 0000000..3ef1fe2 Binary files /dev/null and b/tts/cache/cafeca58c121acabcd6a1a8ceae958f8.wav differ diff --git a/tts/cache/cb7529f1c463edd04fb8d5d082ae270e.wav b/tts/cache/cb7529f1c463edd04fb8d5d082ae270e.wav new file mode 100644 index 0000000..f520c06 Binary files /dev/null and b/tts/cache/cb7529f1c463edd04fb8d5d082ae270e.wav differ diff --git a/tts/cache/cb90dd9ddafe01e042e4352f3a52168b.wav b/tts/cache/cb90dd9ddafe01e042e4352f3a52168b.wav new file mode 100644 index 0000000..57eab31 Binary files /dev/null and b/tts/cache/cb90dd9ddafe01e042e4352f3a52168b.wav differ diff --git a/tts/cache/cbdde1f17141ba5f29037104b7a0f0cd.wav b/tts/cache/cbdde1f17141ba5f29037104b7a0f0cd.wav new file mode 100644 index 0000000..a8726af Binary files /dev/null and b/tts/cache/cbdde1f17141ba5f29037104b7a0f0cd.wav differ diff --git a/tts/cache/cc8b2ca2805687e699f959d28b37dba3.wav b/tts/cache/cc8b2ca2805687e699f959d28b37dba3.wav new file mode 100644 index 0000000..0179dc7 Binary files /dev/null and b/tts/cache/cc8b2ca2805687e699f959d28b37dba3.wav differ diff --git a/tts/cache/ccd24939aa2087abb10830d064715b24.wav b/tts/cache/ccd24939aa2087abb10830d064715b24.wav new file mode 100644 index 0000000..7780c0e Binary files /dev/null and b/tts/cache/ccd24939aa2087abb10830d064715b24.wav differ diff --git a/tts/cache/ccffe9eeef906da1797f40f6ab27407d.wav b/tts/cache/ccffe9eeef906da1797f40f6ab27407d.wav new file mode 100644 index 0000000..abebd70 Binary files /dev/null and b/tts/cache/ccffe9eeef906da1797f40f6ab27407d.wav differ diff --git a/tts/cache/cd32395ff32213fd8f9010dfa9036bf4.wav b/tts/cache/cd32395ff32213fd8f9010dfa9036bf4.wav new file mode 100644 index 0000000..89609d0 Binary files /dev/null and b/tts/cache/cd32395ff32213fd8f9010dfa9036bf4.wav differ diff --git a/tts/cache/cd5a64ddb3b1299ddf36eeec93d3364e.wav b/tts/cache/cd5a64ddb3b1299ddf36eeec93d3364e.wav new file mode 100644 index 0000000..98c7fc6 Binary files /dev/null and b/tts/cache/cd5a64ddb3b1299ddf36eeec93d3364e.wav differ diff --git a/tts/cache/cd8e364b06f6533fb785c4d6a16809e1.wav b/tts/cache/cd8e364b06f6533fb785c4d6a16809e1.wav new file mode 100644 index 0000000..e7969be Binary files /dev/null and b/tts/cache/cd8e364b06f6533fb785c4d6a16809e1.wav differ diff --git a/tts/cache/cdc11068966e13d64a906b91c75c1f28.wav b/tts/cache/cdc11068966e13d64a906b91c75c1f28.wav new file mode 100644 index 0000000..80ca438 Binary files /dev/null and b/tts/cache/cdc11068966e13d64a906b91c75c1f28.wav differ diff --git a/tts/cache/cdf42ef119c39d27c77bd447d9429709.wav b/tts/cache/cdf42ef119c39d27c77bd447d9429709.wav new file mode 100644 index 0000000..8fd7dfc Binary files /dev/null and b/tts/cache/cdf42ef119c39d27c77bd447d9429709.wav differ diff --git a/tts/cache/ce32c69bca48f2b8388fba772bff2b6e.wav b/tts/cache/ce32c69bca48f2b8388fba772bff2b6e.wav new file mode 100644 index 0000000..522121e Binary files /dev/null and b/tts/cache/ce32c69bca48f2b8388fba772bff2b6e.wav differ diff --git a/tts/cache/ceb3db859898b709fd3b75ecfffd0d0d.wav b/tts/cache/ceb3db859898b709fd3b75ecfffd0d0d.wav new file mode 100644 index 0000000..9d91e2e Binary files /dev/null and b/tts/cache/ceb3db859898b709fd3b75ecfffd0d0d.wav differ diff --git a/tts/cache/d016c2b4970a5f9622aebc22c0d18bd9.wav b/tts/cache/d016c2b4970a5f9622aebc22c0d18bd9.wav new file mode 100644 index 0000000..d58828e Binary files /dev/null and b/tts/cache/d016c2b4970a5f9622aebc22c0d18bd9.wav differ diff --git a/tts/cache/d0698c414e43053c64e30ef0121cd1fe.wav b/tts/cache/d0698c414e43053c64e30ef0121cd1fe.wav new file mode 100644 index 0000000..fdc9eb4 Binary files /dev/null and b/tts/cache/d0698c414e43053c64e30ef0121cd1fe.wav differ diff --git a/tts/cache/d10156bb5b3c00f738d7d4ca8f19188c.wav b/tts/cache/d10156bb5b3c00f738d7d4ca8f19188c.wav new file mode 100644 index 0000000..3dc2468 Binary files /dev/null and b/tts/cache/d10156bb5b3c00f738d7d4ca8f19188c.wav differ diff --git a/tts/cache/d1303fe519ca848a157d4dd35f4cab6c.wav b/tts/cache/d1303fe519ca848a157d4dd35f4cab6c.wav new file mode 100644 index 0000000..d60702d Binary files /dev/null and b/tts/cache/d1303fe519ca848a157d4dd35f4cab6c.wav differ diff --git a/tts/cache/d17e00df788d7d5cd3140cba7bbb8d72.wav b/tts/cache/d17e00df788d7d5cd3140cba7bbb8d72.wav new file mode 100644 index 0000000..de86995 Binary files /dev/null and b/tts/cache/d17e00df788d7d5cd3140cba7bbb8d72.wav differ diff --git a/tts/cache/d1e3aa6628f898c7b234bde959914feb.wav b/tts/cache/d1e3aa6628f898c7b234bde959914feb.wav new file mode 100644 index 0000000..d35b3c7 Binary files /dev/null and b/tts/cache/d1e3aa6628f898c7b234bde959914feb.wav differ diff --git a/tts/cache/d221cabe851d28f9367900362f29228d.wav b/tts/cache/d221cabe851d28f9367900362f29228d.wav new file mode 100644 index 0000000..11ba37e Binary files /dev/null and b/tts/cache/d221cabe851d28f9367900362f29228d.wav differ diff --git a/tts/cache/d236e47e25bbd676608835df95cad9bd.wav b/tts/cache/d236e47e25bbd676608835df95cad9bd.wav new file mode 100644 index 0000000..b79cc47 Binary files /dev/null and b/tts/cache/d236e47e25bbd676608835df95cad9bd.wav differ diff --git a/tts/cache/d35f2d5158efc564390abc45cb106cb8.wav b/tts/cache/d35f2d5158efc564390abc45cb106cb8.wav new file mode 100644 index 0000000..998e6d7 Binary files /dev/null and b/tts/cache/d35f2d5158efc564390abc45cb106cb8.wav differ diff --git a/tts/cache/d37b560e0334979db2967d98e0a6af5d.wav b/tts/cache/d37b560e0334979db2967d98e0a6af5d.wav new file mode 100644 index 0000000..07f377d Binary files /dev/null and b/tts/cache/d37b560e0334979db2967d98e0a6af5d.wav differ diff --git a/tts/cache/d3ec21219bd4b4058704e08a2b2a7462.wav b/tts/cache/d3ec21219bd4b4058704e08a2b2a7462.wav new file mode 100644 index 0000000..bad2520 Binary files /dev/null and b/tts/cache/d3ec21219bd4b4058704e08a2b2a7462.wav differ diff --git a/tts/cache/d48a3f68ffacc6a3a6435b9c534f3bdf.wav b/tts/cache/d48a3f68ffacc6a3a6435b9c534f3bdf.wav new file mode 100644 index 0000000..7f7188e Binary files /dev/null and b/tts/cache/d48a3f68ffacc6a3a6435b9c534f3bdf.wav differ diff --git a/tts/cache/d52b2fec70148c25e2de162838b563a2.wav b/tts/cache/d52b2fec70148c25e2de162838b563a2.wav new file mode 100644 index 0000000..08fc0fb Binary files /dev/null and b/tts/cache/d52b2fec70148c25e2de162838b563a2.wav differ diff --git a/tts/cache/d55688c4216c9ed006b72a4ee532e6b3.wav b/tts/cache/d55688c4216c9ed006b72a4ee532e6b3.wav new file mode 100644 index 0000000..799b0d2 Binary files /dev/null and b/tts/cache/d55688c4216c9ed006b72a4ee532e6b3.wav differ diff --git a/tts/cache/d670814e643d5821487c09b9f71ba9f1.wav b/tts/cache/d670814e643d5821487c09b9f71ba9f1.wav new file mode 100644 index 0000000..ac0fd7e Binary files /dev/null and b/tts/cache/d670814e643d5821487c09b9f71ba9f1.wav differ diff --git a/tts/cache/d7e39360dc8be4f1592921ebbfb6c57f.wav b/tts/cache/d7e39360dc8be4f1592921ebbfb6c57f.wav new file mode 100644 index 0000000..53f1003 Binary files /dev/null and b/tts/cache/d7e39360dc8be4f1592921ebbfb6c57f.wav differ diff --git a/tts/cache/d8495ea401b8217e134871af0b71dc82.wav b/tts/cache/d8495ea401b8217e134871af0b71dc82.wav new file mode 100644 index 0000000..dbb32a2 Binary files /dev/null and b/tts/cache/d8495ea401b8217e134871af0b71dc82.wav differ diff --git a/tts/cache/d8bce2a4e5f31ad4534269b5868d65a8.wav b/tts/cache/d8bce2a4e5f31ad4534269b5868d65a8.wav new file mode 100644 index 0000000..9e32768 Binary files /dev/null and b/tts/cache/d8bce2a4e5f31ad4534269b5868d65a8.wav differ diff --git a/tts/cache/d8e3196ad7c0794368f895d5a8a6877a.wav b/tts/cache/d8e3196ad7c0794368f895d5a8a6877a.wav new file mode 100644 index 0000000..19c0efe Binary files /dev/null and b/tts/cache/d8e3196ad7c0794368f895d5a8a6877a.wav differ diff --git a/tts/cache/d96efc1eb3cd1d800bf5017e22640570.wav b/tts/cache/d96efc1eb3cd1d800bf5017e22640570.wav new file mode 100644 index 0000000..1e738e5 Binary files /dev/null and b/tts/cache/d96efc1eb3cd1d800bf5017e22640570.wav differ diff --git a/tts/cache/d97ed85536219c8c1ba5176d7a6818e2.wav b/tts/cache/d97ed85536219c8c1ba5176d7a6818e2.wav new file mode 100644 index 0000000..2ed78f9 Binary files /dev/null and b/tts/cache/d97ed85536219c8c1ba5176d7a6818e2.wav differ diff --git a/tts/cache/d9efc1025b09f5f881a70df248fdf17f.wav b/tts/cache/d9efc1025b09f5f881a70df248fdf17f.wav new file mode 100644 index 0000000..747a3cc Binary files /dev/null and b/tts/cache/d9efc1025b09f5f881a70df248fdf17f.wav differ diff --git a/tts/cache/da3f01c49d2a42b2917265ff51d9c766.wav b/tts/cache/da3f01c49d2a42b2917265ff51d9c766.wav new file mode 100644 index 0000000..71ef7d6 Binary files /dev/null and b/tts/cache/da3f01c49d2a42b2917265ff51d9c766.wav differ diff --git a/tts/cache/dae9bb691d632f7007611ed76daf1cdc.wav b/tts/cache/dae9bb691d632f7007611ed76daf1cdc.wav new file mode 100644 index 0000000..c60a7b9 Binary files /dev/null and b/tts/cache/dae9bb691d632f7007611ed76daf1cdc.wav differ diff --git a/tts/cache/dc341cc93d7ec4b6aadbea7293493400.wav b/tts/cache/dc341cc93d7ec4b6aadbea7293493400.wav new file mode 100644 index 0000000..48f4c0b Binary files /dev/null and b/tts/cache/dc341cc93d7ec4b6aadbea7293493400.wav differ diff --git a/tts/cache/dc9cc2b4db9ec27dc83574561577f348.wav b/tts/cache/dc9cc2b4db9ec27dc83574561577f348.wav new file mode 100644 index 0000000..4f7fb74 Binary files /dev/null and b/tts/cache/dc9cc2b4db9ec27dc83574561577f348.wav differ diff --git a/tts/cache/dcbb871fb1218a7c2a91737b7a01563f.wav b/tts/cache/dcbb871fb1218a7c2a91737b7a01563f.wav new file mode 100644 index 0000000..d1940c9 Binary files /dev/null and b/tts/cache/dcbb871fb1218a7c2a91737b7a01563f.wav differ diff --git a/tts/cache/dd2653054b9979ecc535ddf3e0e8b054.wav b/tts/cache/dd2653054b9979ecc535ddf3e0e8b054.wav new file mode 100644 index 0000000..fd862d0 Binary files /dev/null and b/tts/cache/dd2653054b9979ecc535ddf3e0e8b054.wav differ diff --git a/tts/cache/dd836d4f323658ef415dc1d541147098.wav b/tts/cache/dd836d4f323658ef415dc1d541147098.wav new file mode 100644 index 0000000..bb57d20 Binary files /dev/null and b/tts/cache/dd836d4f323658ef415dc1d541147098.wav differ diff --git a/tts/cache/ddb35a0671ac50e28101c9caec3c906e.wav b/tts/cache/ddb35a0671ac50e28101c9caec3c906e.wav new file mode 100644 index 0000000..65c5306 Binary files /dev/null and b/tts/cache/ddb35a0671ac50e28101c9caec3c906e.wav differ diff --git a/tts/cache/df2c4bae10e8c4419ae1e4405a2d9fd1.wav b/tts/cache/df2c4bae10e8c4419ae1e4405a2d9fd1.wav new file mode 100644 index 0000000..a3b5959 Binary files /dev/null and b/tts/cache/df2c4bae10e8c4419ae1e4405a2d9fd1.wav differ diff --git a/tts/cache/e003164d976b2c0ce07eceaf989047d9.wav b/tts/cache/e003164d976b2c0ce07eceaf989047d9.wav new file mode 100644 index 0000000..ddda405 Binary files /dev/null and b/tts/cache/e003164d976b2c0ce07eceaf989047d9.wav differ diff --git a/tts/cache/e090e9952ae2a455a5bb60416c918e62.wav b/tts/cache/e090e9952ae2a455a5bb60416c918e62.wav new file mode 100644 index 0000000..d407f71 Binary files /dev/null and b/tts/cache/e090e9952ae2a455a5bb60416c918e62.wav differ diff --git a/tts/cache/e0aa7d1ee5043b889522ec7bcffac5be.wav b/tts/cache/e0aa7d1ee5043b889522ec7bcffac5be.wav new file mode 100644 index 0000000..f79c7e7 Binary files /dev/null and b/tts/cache/e0aa7d1ee5043b889522ec7bcffac5be.wav differ diff --git a/tts/cache/e0e36ddbbfa11984dae2a2ae06bb8e5e.wav b/tts/cache/e0e36ddbbfa11984dae2a2ae06bb8e5e.wav new file mode 100644 index 0000000..9b08e77 Binary files /dev/null and b/tts/cache/e0e36ddbbfa11984dae2a2ae06bb8e5e.wav differ diff --git a/tts/cache/e10bd5b82a2449e7844ad0ac017f98b1.wav b/tts/cache/e10bd5b82a2449e7844ad0ac017f98b1.wav new file mode 100644 index 0000000..6475152 Binary files /dev/null and b/tts/cache/e10bd5b82a2449e7844ad0ac017f98b1.wav differ diff --git a/tts/cache/e1defa1d453861a8001819b4c2025e82.wav b/tts/cache/e1defa1d453861a8001819b4c2025e82.wav new file mode 100644 index 0000000..6fa4759 Binary files /dev/null and b/tts/cache/e1defa1d453861a8001819b4c2025e82.wav differ diff --git a/tts/cache/e248afd297a75632a4bf873af5cd5f37.wav b/tts/cache/e248afd297a75632a4bf873af5cd5f37.wav new file mode 100644 index 0000000..80d4703 Binary files /dev/null and b/tts/cache/e248afd297a75632a4bf873af5cd5f37.wav differ diff --git a/tts/cache/e4d5585f3f066e330e77933f9ec0671b.wav b/tts/cache/e4d5585f3f066e330e77933f9ec0671b.wav new file mode 100644 index 0000000..91b10fa Binary files /dev/null and b/tts/cache/e4d5585f3f066e330e77933f9ec0671b.wav differ diff --git a/tts/cache/e5e15a3c235e9964c6a56afdd795ffee.wav b/tts/cache/e5e15a3c235e9964c6a56afdd795ffee.wav new file mode 100644 index 0000000..950ae74 Binary files /dev/null and b/tts/cache/e5e15a3c235e9964c6a56afdd795ffee.wav differ diff --git a/tts/cache/e70a5a82ae45dd55df879c0c70c76650.wav b/tts/cache/e70a5a82ae45dd55df879c0c70c76650.wav new file mode 100644 index 0000000..5d85096 Binary files /dev/null and b/tts/cache/e70a5a82ae45dd55df879c0c70c76650.wav differ diff --git a/tts/cache/e7568458d49e977a149152e1e6810dae.wav b/tts/cache/e7568458d49e977a149152e1e6810dae.wav new file mode 100644 index 0000000..fad3a36 Binary files /dev/null and b/tts/cache/e7568458d49e977a149152e1e6810dae.wav differ diff --git a/tts/cache/e79aeab1bf4d9ce9f8c0dc09aa181181.wav b/tts/cache/e79aeab1bf4d9ce9f8c0dc09aa181181.wav new file mode 100644 index 0000000..46bc0ce Binary files /dev/null and b/tts/cache/e79aeab1bf4d9ce9f8c0dc09aa181181.wav differ diff --git a/tts/cache/e9258826f94bd8c237943d5645061e1a.wav b/tts/cache/e9258826f94bd8c237943d5645061e1a.wav new file mode 100644 index 0000000..0ab9c34 Binary files /dev/null and b/tts/cache/e9258826f94bd8c237943d5645061e1a.wav differ diff --git a/tts/cache/e9df782c5005a3dea494f4829e71730d.wav b/tts/cache/e9df782c5005a3dea494f4829e71730d.wav new file mode 100644 index 0000000..f8d5ae7 Binary files /dev/null and b/tts/cache/e9df782c5005a3dea494f4829e71730d.wav differ diff --git a/tts/cache/ea1a612a75646b5532df94c890a32ec1.wav b/tts/cache/ea1a612a75646b5532df94c890a32ec1.wav new file mode 100644 index 0000000..d6fb1e3 Binary files /dev/null and b/tts/cache/ea1a612a75646b5532df94c890a32ec1.wav differ diff --git a/tts/cache/ea425da70aa75f0b6b294d7f6a890613.wav b/tts/cache/ea425da70aa75f0b6b294d7f6a890613.wav new file mode 100644 index 0000000..7584688 Binary files /dev/null and b/tts/cache/ea425da70aa75f0b6b294d7f6a890613.wav differ diff --git a/tts/cache/eaf9c67c3d5cd6d2da82ea2b2bb05e56.wav b/tts/cache/eaf9c67c3d5cd6d2da82ea2b2bb05e56.wav new file mode 100644 index 0000000..47410b8 Binary files /dev/null and b/tts/cache/eaf9c67c3d5cd6d2da82ea2b2bb05e56.wav differ diff --git a/tts/cache/eafc332a469a16a7e55b59fcb397ed5c.wav b/tts/cache/eafc332a469a16a7e55b59fcb397ed5c.wav new file mode 100644 index 0000000..463305c Binary files /dev/null and b/tts/cache/eafc332a469a16a7e55b59fcb397ed5c.wav differ diff --git a/tts/cache/eb9da0ed13c7c62d21a8bed364a709d6.wav b/tts/cache/eb9da0ed13c7c62d21a8bed364a709d6.wav new file mode 100644 index 0000000..a5bc680 Binary files /dev/null and b/tts/cache/eb9da0ed13c7c62d21a8bed364a709d6.wav differ diff --git a/tts/cache/ebc1eda8ee2ac8e85147da428250a771.wav b/tts/cache/ebc1eda8ee2ac8e85147da428250a771.wav new file mode 100644 index 0000000..baf47df Binary files /dev/null and b/tts/cache/ebc1eda8ee2ac8e85147da428250a771.wav differ diff --git a/tts/cache/ebc4dfcdf6a73bbb9776973f585e127c.wav b/tts/cache/ebc4dfcdf6a73bbb9776973f585e127c.wav new file mode 100644 index 0000000..d33b14b Binary files /dev/null and b/tts/cache/ebc4dfcdf6a73bbb9776973f585e127c.wav differ diff --git a/tts/cache/ebe54a230f3a29075c55a32785442532.wav b/tts/cache/ebe54a230f3a29075c55a32785442532.wav new file mode 100644 index 0000000..9fe0449 Binary files /dev/null and b/tts/cache/ebe54a230f3a29075c55a32785442532.wav differ diff --git a/tts/cache/ece8cc6fcbe0d415a8b28f961f4a4fa7.wav b/tts/cache/ece8cc6fcbe0d415a8b28f961f4a4fa7.wav new file mode 100644 index 0000000..4ee98dd Binary files /dev/null and b/tts/cache/ece8cc6fcbe0d415a8b28f961f4a4fa7.wav differ diff --git a/tts/cache/ecf03f40b7da99541c021087c8cbb7da.wav b/tts/cache/ecf03f40b7da99541c021087c8cbb7da.wav new file mode 100644 index 0000000..a8c3074 Binary files /dev/null and b/tts/cache/ecf03f40b7da99541c021087c8cbb7da.wav differ diff --git a/tts/cache/ed0a4f5c2f54d1e8ad4aee4d353d03ae.wav b/tts/cache/ed0a4f5c2f54d1e8ad4aee4d353d03ae.wav new file mode 100644 index 0000000..6460d3c Binary files /dev/null and b/tts/cache/ed0a4f5c2f54d1e8ad4aee4d353d03ae.wav differ diff --git a/tts/cache/ed0c7ebf3e7576114893b4712ef9c816.wav b/tts/cache/ed0c7ebf3e7576114893b4712ef9c816.wav new file mode 100644 index 0000000..bc743b2 Binary files /dev/null and b/tts/cache/ed0c7ebf3e7576114893b4712ef9c816.wav differ diff --git a/tts/cache/ee78b26d5437bba5e2736e4bd40f717f.wav b/tts/cache/ee78b26d5437bba5e2736e4bd40f717f.wav new file mode 100644 index 0000000..ea067f6 Binary files /dev/null and b/tts/cache/ee78b26d5437bba5e2736e4bd40f717f.wav differ diff --git a/tts/cache/efd537995ff9848ce4e15f15ebe35732.wav b/tts/cache/efd537995ff9848ce4e15f15ebe35732.wav new file mode 100644 index 0000000..028242e Binary files /dev/null and b/tts/cache/efd537995ff9848ce4e15f15ebe35732.wav differ diff --git a/tts/cache/efe932f9501557732e6d303a7351a438.wav b/tts/cache/efe932f9501557732e6d303a7351a438.wav new file mode 100644 index 0000000..de5deb5 Binary files /dev/null and b/tts/cache/efe932f9501557732e6d303a7351a438.wav differ diff --git a/tts/cache/f0085b0af40a7cd92ddb0ba9e9b70f6b.wav b/tts/cache/f0085b0af40a7cd92ddb0ba9e9b70f6b.wav new file mode 100644 index 0000000..5833d78 Binary files /dev/null and b/tts/cache/f0085b0af40a7cd92ddb0ba9e9b70f6b.wav differ diff --git a/tts/cache/f1cbe0ac7f6ec8abc18952eba7d00b37.wav b/tts/cache/f1cbe0ac7f6ec8abc18952eba7d00b37.wav new file mode 100644 index 0000000..9b43075 Binary files /dev/null and b/tts/cache/f1cbe0ac7f6ec8abc18952eba7d00b37.wav differ diff --git a/tts/cache/f224ab9843d420c27424ad3e26473cc8.wav b/tts/cache/f224ab9843d420c27424ad3e26473cc8.wav new file mode 100644 index 0000000..3072286 Binary files /dev/null and b/tts/cache/f224ab9843d420c27424ad3e26473cc8.wav differ diff --git a/tts/cache/f3293223544a681a276ec3bbe2788684.wav b/tts/cache/f3293223544a681a276ec3bbe2788684.wav new file mode 100644 index 0000000..1ff972f Binary files /dev/null and b/tts/cache/f3293223544a681a276ec3bbe2788684.wav differ diff --git a/tts/cache/f3a33e3ed63f2e879e3591494e1e0008.wav b/tts/cache/f3a33e3ed63f2e879e3591494e1e0008.wav new file mode 100644 index 0000000..99bad96 Binary files /dev/null and b/tts/cache/f3a33e3ed63f2e879e3591494e1e0008.wav differ diff --git a/tts/cache/f3c94b8e616e7dfd332ff1c5fdec366e.wav b/tts/cache/f3c94b8e616e7dfd332ff1c5fdec366e.wav new file mode 100644 index 0000000..bd3d07b Binary files /dev/null and b/tts/cache/f3c94b8e616e7dfd332ff1c5fdec366e.wav differ diff --git a/tts/cache/f4c366bf3a677733faf087931d372c15.wav b/tts/cache/f4c366bf3a677733faf087931d372c15.wav new file mode 100644 index 0000000..bb3815b Binary files /dev/null and b/tts/cache/f4c366bf3a677733faf087931d372c15.wav differ diff --git a/tts/cache/f58e6648e69b1337ef11020a2f973e0e.wav b/tts/cache/f58e6648e69b1337ef11020a2f973e0e.wav new file mode 100644 index 0000000..7db0190 Binary files /dev/null and b/tts/cache/f58e6648e69b1337ef11020a2f973e0e.wav differ diff --git a/tts/cache/f5fdb926f766b2bcbe31232e28eaf0ad.wav b/tts/cache/f5fdb926f766b2bcbe31232e28eaf0ad.wav new file mode 100644 index 0000000..f307914 Binary files /dev/null and b/tts/cache/f5fdb926f766b2bcbe31232e28eaf0ad.wav differ diff --git a/tts/cache/f6a47d08f41474dec77ff62c82408a4f.wav b/tts/cache/f6a47d08f41474dec77ff62c82408a4f.wav new file mode 100644 index 0000000..c77b7ba Binary files /dev/null and b/tts/cache/f6a47d08f41474dec77ff62c82408a4f.wav differ diff --git a/tts/cache/f6af8f30a51062a9bc3ba5f740742158.wav b/tts/cache/f6af8f30a51062a9bc3ba5f740742158.wav new file mode 100644 index 0000000..d75e643 Binary files /dev/null and b/tts/cache/f6af8f30a51062a9bc3ba5f740742158.wav differ diff --git a/tts/cache/f7bac05b80c42c18716abf0f68e0f72c.wav b/tts/cache/f7bac05b80c42c18716abf0f68e0f72c.wav new file mode 100644 index 0000000..b64d030 Binary files /dev/null and b/tts/cache/f7bac05b80c42c18716abf0f68e0f72c.wav differ diff --git a/tts/cache/f7dccc1c2c35cb282cc0d50118bcd214.wav b/tts/cache/f7dccc1c2c35cb282cc0d50118bcd214.wav new file mode 100644 index 0000000..4fefa8b Binary files /dev/null and b/tts/cache/f7dccc1c2c35cb282cc0d50118bcd214.wav differ diff --git a/tts/cache/f8b3a750bc86c2f08dcfea9347e05f74.wav b/tts/cache/f8b3a750bc86c2f08dcfea9347e05f74.wav new file mode 100644 index 0000000..aed8052 Binary files /dev/null and b/tts/cache/f8b3a750bc86c2f08dcfea9347e05f74.wav differ diff --git a/tts/cache/f8ddcbe366e2e0a33ae9abac249b56c9.wav b/tts/cache/f8ddcbe366e2e0a33ae9abac249b56c9.wav new file mode 100644 index 0000000..341c20f Binary files /dev/null and b/tts/cache/f8ddcbe366e2e0a33ae9abac249b56c9.wav differ diff --git a/tts/cache/f9c012f8f33d0144f5688223d1ad440b.wav b/tts/cache/f9c012f8f33d0144f5688223d1ad440b.wav new file mode 100644 index 0000000..0ce0608 Binary files /dev/null and b/tts/cache/f9c012f8f33d0144f5688223d1ad440b.wav differ diff --git a/tts/cache/fa988c7c416b76a6a3102ffbb5c34e7b.wav b/tts/cache/fa988c7c416b76a6a3102ffbb5c34e7b.wav new file mode 100644 index 0000000..37abb92 Binary files /dev/null and b/tts/cache/fa988c7c416b76a6a3102ffbb5c34e7b.wav differ diff --git a/tts/cache/fb47493e84e406657592d26616e539dc.wav b/tts/cache/fb47493e84e406657592d26616e539dc.wav new file mode 100644 index 0000000..a56ade6 Binary files /dev/null and b/tts/cache/fb47493e84e406657592d26616e539dc.wav differ diff --git a/tts/cache/fbe0f57dad334e8b015ec570ecb2a4df.wav b/tts/cache/fbe0f57dad334e8b015ec570ecb2a4df.wav new file mode 100644 index 0000000..a51c230 Binary files /dev/null and b/tts/cache/fbe0f57dad334e8b015ec570ecb2a4df.wav differ diff --git a/tts/cache/fc39114fa8636835e39d454370ae799b.wav b/tts/cache/fc39114fa8636835e39d454370ae799b.wav new file mode 100644 index 0000000..c2a2590 Binary files /dev/null and b/tts/cache/fc39114fa8636835e39d454370ae799b.wav differ diff --git a/tts/cache/fcc477494a551cf7265e463a39761be6.wav b/tts/cache/fcc477494a551cf7265e463a39761be6.wav new file mode 100644 index 0000000..e0fb3b1 Binary files /dev/null and b/tts/cache/fcc477494a551cf7265e463a39761be6.wav differ diff --git a/tts/cache/fd2ce995681e53d02af4252296deef52.wav b/tts/cache/fd2ce995681e53d02af4252296deef52.wav new file mode 100644 index 0000000..2421b5d Binary files /dev/null and b/tts/cache/fd2ce995681e53d02af4252296deef52.wav differ diff --git a/tts/cache/fd2dd01cb93095bd756b90784fb79295.wav b/tts/cache/fd2dd01cb93095bd756b90784fb79295.wav new file mode 100644 index 0000000..ddf8814 Binary files /dev/null and b/tts/cache/fd2dd01cb93095bd756b90784fb79295.wav differ diff --git a/tts/cache/fd5d6e35fe495776250eb74116593858.wav b/tts/cache/fd5d6e35fe495776250eb74116593858.wav new file mode 100644 index 0000000..3e002e9 Binary files /dev/null and b/tts/cache/fd5d6e35fe495776250eb74116593858.wav differ diff --git a/tts/cache/fd8fc539bc32ff0a5eb78978a171badc.wav b/tts/cache/fd8fc539bc32ff0a5eb78978a171badc.wav new file mode 100644 index 0000000..86ca151 Binary files /dev/null and b/tts/cache/fd8fc539bc32ff0a5eb78978a171badc.wav differ diff --git a/tts/cache/fe64e0101c58f9b3a47b28b3f5da9671.wav b/tts/cache/fe64e0101c58f9b3a47b28b3f5da9671.wav new file mode 100644 index 0000000..242c6c6 Binary files /dev/null and b/tts/cache/fe64e0101c58f9b3a47b28b3f5da9671.wav differ diff --git a/tts/cache/fe7f514803f7b06a85e95cd8a80407cf.wav b/tts/cache/fe7f514803f7b06a85e95cd8a80407cf.wav new file mode 100644 index 0000000..e4c0385 Binary files /dev/null and b/tts/cache/fe7f514803f7b06a85e95cd8a80407cf.wav differ diff --git a/tts/cache/fef2df0b1558fad54324829427769afa.wav b/tts/cache/fef2df0b1558fad54324829427769afa.wav new file mode 100644 index 0000000..d95153c Binary files /dev/null and b/tts/cache/fef2df0b1558fad54324829427769afa.wav differ diff --git a/tts/cache/ffa60d61b67e2bd6f6958b786ab85aef.wav b/tts/cache/ffa60d61b67e2bd6f6958b786ab85aef.wav new file mode 100644 index 0000000..44830c6 Binary files /dev/null and b/tts/cache/ffa60d61b67e2bd6f6958b786ab85aef.wav differ diff --git a/tts/cache/ffdb4c989461f285d538ef6005d27583.wav b/tts/cache/ffdb4c989461f285d538ef6005d27583.wav new file mode 100644 index 0000000..122a709 Binary files /dev/null and b/tts/cache/ffdb4c989461f285d538ef6005d27583.wav differ diff --git a/tts/cache/ffdcc66ef27cb3eb515dabf8f2c6ecf5.wav b/tts/cache/ffdcc66ef27cb3eb515dabf8f2c6ecf5.wav new file mode 100644 index 0000000..7731894 Binary files /dev/null and b/tts/cache/ffdcc66ef27cb3eb515dabf8f2c6ecf5.wav differ diff --git a/tts/piper/espeak-ng b/tts/piper/espeak-ng new file mode 100644 index 0000000..6de664f Binary files /dev/null and b/tts/piper/espeak-ng differ diff --git a/tts/piper/espeak-ng-data/af_dict b/tts/piper/espeak-ng-data/af_dict new file mode 100644 index 0000000..b6259ef Binary files /dev/null and b/tts/piper/espeak-ng-data/af_dict differ diff --git a/tts/piper/espeak-ng-data/am_dict b/tts/piper/espeak-ng-data/am_dict new file mode 100644 index 0000000..b2b0442 Binary files /dev/null and b/tts/piper/espeak-ng-data/am_dict differ diff --git a/tts/piper/espeak-ng-data/an_dict b/tts/piper/espeak-ng-data/an_dict new file mode 100644 index 0000000..d10d32c Binary files /dev/null and b/tts/piper/espeak-ng-data/an_dict differ diff --git a/tts/piper/espeak-ng-data/ar_dict b/tts/piper/espeak-ng-data/ar_dict new file mode 100644 index 0000000..108ff2b Binary files /dev/null and b/tts/piper/espeak-ng-data/ar_dict differ diff --git a/tts/piper/espeak-ng-data/as_dict b/tts/piper/espeak-ng-data/as_dict new file mode 100644 index 0000000..46108a8 Binary files /dev/null and b/tts/piper/espeak-ng-data/as_dict differ diff --git a/tts/piper/espeak-ng-data/az_dict b/tts/piper/espeak-ng-data/az_dict new file mode 100644 index 0000000..77ca091 Binary files /dev/null and b/tts/piper/espeak-ng-data/az_dict differ diff --git a/tts/piper/espeak-ng-data/ba_dict b/tts/piper/espeak-ng-data/ba_dict new file mode 100644 index 0000000..f175c68 Binary files /dev/null and b/tts/piper/espeak-ng-data/ba_dict differ diff --git a/tts/piper/espeak-ng-data/be_dict b/tts/piper/espeak-ng-data/be_dict new file mode 100644 index 0000000..39d7434 Binary files /dev/null and b/tts/piper/espeak-ng-data/be_dict differ diff --git a/tts/piper/espeak-ng-data/bg_dict b/tts/piper/espeak-ng-data/bg_dict new file mode 100644 index 0000000..ecd7f4a Binary files /dev/null and b/tts/piper/espeak-ng-data/bg_dict differ diff --git a/tts/piper/espeak-ng-data/bn_dict b/tts/piper/espeak-ng-data/bn_dict new file mode 100644 index 0000000..627f13e Binary files /dev/null and b/tts/piper/espeak-ng-data/bn_dict differ diff --git a/tts/piper/espeak-ng-data/bpy_dict b/tts/piper/espeak-ng-data/bpy_dict new file mode 100644 index 0000000..735496f Binary files /dev/null and b/tts/piper/espeak-ng-data/bpy_dict differ diff --git a/tts/piper/espeak-ng-data/bs_dict b/tts/piper/espeak-ng-data/bs_dict new file mode 100644 index 0000000..71a3893 Binary files /dev/null and b/tts/piper/espeak-ng-data/bs_dict differ diff --git a/tts/piper/espeak-ng-data/ca_dict b/tts/piper/espeak-ng-data/ca_dict new file mode 100644 index 0000000..f134d4e Binary files /dev/null and b/tts/piper/espeak-ng-data/ca_dict differ diff --git a/tts/piper/espeak-ng-data/chr_dict b/tts/piper/espeak-ng-data/chr_dict new file mode 100644 index 0000000..47bc095 Binary files /dev/null and b/tts/piper/espeak-ng-data/chr_dict differ diff --git a/tts/piper/espeak-ng-data/cmn_dict b/tts/piper/espeak-ng-data/cmn_dict new file mode 100644 index 0000000..f284cab Binary files /dev/null and b/tts/piper/espeak-ng-data/cmn_dict differ diff --git a/tts/piper/espeak-ng-data/cs_dict b/tts/piper/espeak-ng-data/cs_dict new file mode 100644 index 0000000..cf916da Binary files /dev/null and b/tts/piper/espeak-ng-data/cs_dict differ diff --git a/tts/piper/espeak-ng-data/cv_dict b/tts/piper/espeak-ng-data/cv_dict new file mode 100644 index 0000000..79311d9 Binary files /dev/null and b/tts/piper/espeak-ng-data/cv_dict differ diff --git a/tts/piper/espeak-ng-data/cy_dict b/tts/piper/espeak-ng-data/cy_dict new file mode 100644 index 0000000..c4f738b Binary files /dev/null and b/tts/piper/espeak-ng-data/cy_dict differ diff --git a/tts/piper/espeak-ng-data/da_dict b/tts/piper/espeak-ng-data/da_dict new file mode 100644 index 0000000..357bdee Binary files /dev/null and b/tts/piper/espeak-ng-data/da_dict differ diff --git a/tts/piper/espeak-ng-data/de_dict b/tts/piper/espeak-ng-data/de_dict new file mode 100644 index 0000000..6f7697c Binary files /dev/null and b/tts/piper/espeak-ng-data/de_dict differ diff --git a/tts/piper/espeak-ng-data/el_dict b/tts/piper/espeak-ng-data/el_dict new file mode 100644 index 0000000..e6831f0 Binary files /dev/null and b/tts/piper/espeak-ng-data/el_dict differ diff --git a/tts/piper/espeak-ng-data/en_dict b/tts/piper/espeak-ng-data/en_dict new file mode 100644 index 0000000..b05b861 Binary files /dev/null and b/tts/piper/espeak-ng-data/en_dict differ diff --git a/tts/piper/espeak-ng-data/eo_dict b/tts/piper/espeak-ng-data/eo_dict new file mode 100644 index 0000000..4ea54fe Binary files /dev/null and b/tts/piper/espeak-ng-data/eo_dict differ diff --git a/tts/piper/espeak-ng-data/es_dict b/tts/piper/espeak-ng-data/es_dict new file mode 100644 index 0000000..c810ac7 Binary files /dev/null and b/tts/piper/espeak-ng-data/es_dict differ diff --git a/tts/piper/espeak-ng-data/et_dict b/tts/piper/espeak-ng-data/et_dict new file mode 100644 index 0000000..f336d14 Binary files /dev/null and b/tts/piper/espeak-ng-data/et_dict differ diff --git a/tts/piper/espeak-ng-data/eu_dict b/tts/piper/espeak-ng-data/eu_dict new file mode 100644 index 0000000..ff71e05 Binary files /dev/null and b/tts/piper/espeak-ng-data/eu_dict differ diff --git a/tts/piper/espeak-ng-data/fa_dict b/tts/piper/espeak-ng-data/fa_dict new file mode 100644 index 0000000..c67fb89 Binary files /dev/null and b/tts/piper/espeak-ng-data/fa_dict differ diff --git a/tts/piper/espeak-ng-data/fi_dict b/tts/piper/espeak-ng-data/fi_dict new file mode 100644 index 0000000..a602847 Binary files /dev/null and b/tts/piper/espeak-ng-data/fi_dict differ diff --git a/tts/piper/espeak-ng-data/fr_dict b/tts/piper/espeak-ng-data/fr_dict new file mode 100644 index 0000000..560a8c6 Binary files /dev/null and b/tts/piper/espeak-ng-data/fr_dict differ diff --git a/tts/piper/espeak-ng-data/ga_dict b/tts/piper/espeak-ng-data/ga_dict new file mode 100644 index 0000000..65338e2 Binary files /dev/null and b/tts/piper/espeak-ng-data/ga_dict differ diff --git a/tts/piper/espeak-ng-data/gd_dict b/tts/piper/espeak-ng-data/gd_dict new file mode 100644 index 0000000..3ce4b95 Binary files /dev/null and b/tts/piper/espeak-ng-data/gd_dict differ diff --git a/tts/piper/espeak-ng-data/gn_dict b/tts/piper/espeak-ng-data/gn_dict new file mode 100644 index 0000000..ee7fa4b Binary files /dev/null and b/tts/piper/espeak-ng-data/gn_dict differ diff --git a/tts/piper/espeak-ng-data/grc_dict b/tts/piper/espeak-ng-data/grc_dict new file mode 100644 index 0000000..1d9f374 Binary files /dev/null and b/tts/piper/espeak-ng-data/grc_dict differ diff --git a/tts/piper/espeak-ng-data/gu_dict b/tts/piper/espeak-ng-data/gu_dict new file mode 100644 index 0000000..245afbb Binary files /dev/null and b/tts/piper/espeak-ng-data/gu_dict differ diff --git a/tts/piper/espeak-ng-data/hak_dict b/tts/piper/espeak-ng-data/hak_dict new file mode 100644 index 0000000..38adf87 Binary files /dev/null and b/tts/piper/espeak-ng-data/hak_dict differ diff --git a/tts/piper/espeak-ng-data/haw_dict b/tts/piper/espeak-ng-data/haw_dict new file mode 100644 index 0000000..a78245c Binary files /dev/null and b/tts/piper/espeak-ng-data/haw_dict differ diff --git a/tts/piper/espeak-ng-data/he_dict b/tts/piper/espeak-ng-data/he_dict new file mode 100644 index 0000000..e1037a6 Binary files /dev/null and b/tts/piper/espeak-ng-data/he_dict differ diff --git a/tts/piper/espeak-ng-data/hi_dict b/tts/piper/espeak-ng-data/hi_dict new file mode 100644 index 0000000..c612100 Binary files /dev/null and b/tts/piper/espeak-ng-data/hi_dict differ diff --git a/tts/piper/espeak-ng-data/hr_dict b/tts/piper/espeak-ng-data/hr_dict new file mode 100644 index 0000000..0ecec2d Binary files /dev/null and b/tts/piper/espeak-ng-data/hr_dict differ diff --git a/tts/piper/espeak-ng-data/ht_dict b/tts/piper/espeak-ng-data/ht_dict new file mode 100644 index 0000000..ada2b70 Binary files /dev/null and b/tts/piper/espeak-ng-data/ht_dict differ diff --git a/tts/piper/espeak-ng-data/hu_dict b/tts/piper/espeak-ng-data/hu_dict new file mode 100644 index 0000000..560dc2a Binary files /dev/null and b/tts/piper/espeak-ng-data/hu_dict differ diff --git a/tts/piper/espeak-ng-data/hy_dict b/tts/piper/espeak-ng-data/hy_dict new file mode 100644 index 0000000..7fb0fe3 Binary files /dev/null and b/tts/piper/espeak-ng-data/hy_dict differ diff --git a/tts/piper/espeak-ng-data/ia_dict b/tts/piper/espeak-ng-data/ia_dict new file mode 100644 index 0000000..2177d15 Binary files /dev/null and b/tts/piper/espeak-ng-data/ia_dict differ diff --git a/tts/piper/espeak-ng-data/id_dict b/tts/piper/espeak-ng-data/id_dict new file mode 100644 index 0000000..6e71d72 Binary files /dev/null and b/tts/piper/espeak-ng-data/id_dict differ diff --git a/tts/piper/espeak-ng-data/intonations b/tts/piper/espeak-ng-data/intonations new file mode 100644 index 0000000..ca2d4b3 Binary files /dev/null and b/tts/piper/espeak-ng-data/intonations differ diff --git a/tts/piper/espeak-ng-data/io_dict b/tts/piper/espeak-ng-data/io_dict new file mode 100644 index 0000000..e690ae8 Binary files /dev/null and b/tts/piper/espeak-ng-data/io_dict differ diff --git a/tts/piper/espeak-ng-data/is_dict b/tts/piper/espeak-ng-data/is_dict new file mode 100644 index 0000000..935799a Binary files /dev/null and b/tts/piper/espeak-ng-data/is_dict differ diff --git a/tts/piper/espeak-ng-data/it_dict b/tts/piper/espeak-ng-data/it_dict new file mode 100644 index 0000000..aef22eb Binary files /dev/null and b/tts/piper/espeak-ng-data/it_dict differ diff --git a/tts/piper/espeak-ng-data/ja_dict b/tts/piper/espeak-ng-data/ja_dict new file mode 100644 index 0000000..83bdee5 Binary files /dev/null and b/tts/piper/espeak-ng-data/ja_dict differ diff --git a/tts/piper/espeak-ng-data/jbo_dict b/tts/piper/espeak-ng-data/jbo_dict new file mode 100644 index 0000000..938b323 Binary files /dev/null and b/tts/piper/espeak-ng-data/jbo_dict differ diff --git a/tts/piper/espeak-ng-data/ka_dict b/tts/piper/espeak-ng-data/ka_dict new file mode 100644 index 0000000..a43848b Binary files /dev/null and b/tts/piper/espeak-ng-data/ka_dict differ diff --git a/tts/piper/espeak-ng-data/kk_dict b/tts/piper/espeak-ng-data/kk_dict new file mode 100644 index 0000000..7e02e22 Binary files /dev/null and b/tts/piper/espeak-ng-data/kk_dict differ diff --git a/tts/piper/espeak-ng-data/kl_dict b/tts/piper/espeak-ng-data/kl_dict new file mode 100644 index 0000000..10a0566 Binary files /dev/null and b/tts/piper/espeak-ng-data/kl_dict differ diff --git a/tts/piper/espeak-ng-data/kn_dict b/tts/piper/espeak-ng-data/kn_dict new file mode 100644 index 0000000..6a41a8d Binary files /dev/null and b/tts/piper/espeak-ng-data/kn_dict differ diff --git a/tts/piper/espeak-ng-data/ko_dict b/tts/piper/espeak-ng-data/ko_dict new file mode 100644 index 0000000..2a689d4 Binary files /dev/null and b/tts/piper/espeak-ng-data/ko_dict differ diff --git a/tts/piper/espeak-ng-data/kok_dict b/tts/piper/espeak-ng-data/kok_dict new file mode 100644 index 0000000..4aff010 Binary files /dev/null and b/tts/piper/espeak-ng-data/kok_dict differ diff --git a/tts/piper/espeak-ng-data/ku_dict b/tts/piper/espeak-ng-data/ku_dict new file mode 100644 index 0000000..bf3d745 Binary files /dev/null and b/tts/piper/espeak-ng-data/ku_dict differ diff --git a/tts/piper/espeak-ng-data/ky_dict b/tts/piper/espeak-ng-data/ky_dict new file mode 100644 index 0000000..3ea39d9 Binary files /dev/null and b/tts/piper/espeak-ng-data/ky_dict differ diff --git a/tts/piper/espeak-ng-data/la_dict b/tts/piper/espeak-ng-data/la_dict new file mode 100644 index 0000000..3f30fd3 Binary files /dev/null and b/tts/piper/espeak-ng-data/la_dict differ diff --git a/tts/piper/espeak-ng-data/lang/aav/vi b/tts/piper/espeak-ng-data/lang/aav/vi new file mode 100644 index 0000000..7808128 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/aav/vi @@ -0,0 +1,8 @@ +name Vietnamese (Northern) +language vi + +words 1 2 +pitch 95 175 + + +tone 100 225 800 100 2000 50 5400 75 8000 200 diff --git a/tts/piper/espeak-ng-data/lang/aav/vi-VN-x-central b/tts/piper/espeak-ng-data/lang/aav/vi-VN-x-central new file mode 100644 index 0000000..0defaca --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/aav/vi-VN-x-central @@ -0,0 +1,9 @@ +name Vietnamese (Central) +language vi-vn-x-central +phonemes vi-hue +dictrules 1 + +words 1 +pitch 82 118 //80 118 + voicing 90 //18 + flutter 20 diff --git a/tts/piper/espeak-ng-data/lang/aav/vi-VN-x-south b/tts/piper/espeak-ng-data/lang/aav/vi-VN-x-south new file mode 100644 index 0000000..4c32561 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/aav/vi-VN-x-south @@ -0,0 +1,9 @@ +name Vietnamese (Southern) +language vi-vn-x-south +phonemes vi-sgn +dictrules 2 + +words 1 +pitch 82 118 //80 118 + voicing 90 //18 + flutter 20 diff --git a/tts/piper/espeak-ng-data/lang/art/eo b/tts/piper/espeak-ng-data/lang/art/eo new file mode 100644 index 0000000..e47501f --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/art/eo @@ -0,0 +1,4 @@ +name Esperanto +language eo + +apostrophe 2 diff --git a/tts/piper/espeak-ng-data/lang/art/ia b/tts/piper/espeak-ng-data/lang/art/ia new file mode 100644 index 0000000..6a14728 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/art/ia @@ -0,0 +1,2 @@ +name Interlingua +language ia diff --git a/tts/piper/espeak-ng-data/lang/art/io b/tts/piper/espeak-ng-data/lang/art/io new file mode 100644 index 0000000..c50f9fa --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/art/io @@ -0,0 +1,5 @@ +name Ido +language io +phonemes eo +status testing + diff --git a/tts/piper/espeak-ng-data/lang/art/jbo b/tts/piper/espeak-ng-data/lang/art/jbo new file mode 100644 index 0000000..9cf2f30 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/art/jbo @@ -0,0 +1,4 @@ +name Lojban +language jbo + +speed 80 // speed adjustment, percentage diff --git a/tts/piper/espeak-ng-data/lang/art/lfn b/tts/piper/espeak-ng-data/lang/art/lfn new file mode 100644 index 0000000..d5e4387 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/art/lfn @@ -0,0 +1,8 @@ +name Lingua Franca Nova +language lfn + +phonemes base2 +l_unpronouncable 0 +numbers 2 3 + +stressLength 150 140 180 180 0 0 200 200 diff --git a/tts/piper/espeak-ng-data/lang/art/piqd b/tts/piper/espeak-ng-data/lang/art/piqd new file mode 100644 index 0000000..49367cd --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/art/piqd @@ -0,0 +1,5 @@ +name Klingon +language piqd +status testing +stressRule 3 + diff --git a/tts/piper/espeak-ng-data/lang/art/py b/tts/piper/espeak-ng-data/lang/art/py new file mode 100644 index 0000000..850a34e --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/art/py @@ -0,0 +1,7 @@ +name Pyash +language py +maintainer Logan Streondj +status testing + +speed 80 // speed adjustment, percentage +stressRule 0 diff --git a/tts/piper/espeak-ng-data/lang/art/qdb b/tts/piper/espeak-ng-data/lang/art/qdb new file mode 100644 index 0000000..eb5ea36 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/art/qdb @@ -0,0 +1,6 @@ +name Lang Belta +language qdb + +numbers 4 3 + +replace 1 t ? diff --git a/tts/piper/espeak-ng-data/lang/art/qya b/tts/piper/espeak-ng-data/lang/art/qya new file mode 100644 index 0000000..2b51581 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/art/qya @@ -0,0 +1,4 @@ +name Quenya +language qya +stressRule 2 +// rule=penultimate, with qya_rules for light penultimate syllables to move primary stress to the preceding (antepenultimate) syllable diff --git a/tts/piper/espeak-ng-data/lang/art/sjn b/tts/piper/espeak-ng-data/lang/art/sjn new file mode 100644 index 0000000..f927bc9 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/art/sjn @@ -0,0 +1,4 @@ +name Sindarin +language sjn +stressRule 2 +// rule=penultimate, with sjn_rules for light penultimate syllables to move primary stress to the preceding (antepenultimate) syllable diff --git a/tts/piper/espeak-ng-data/lang/azc/nci b/tts/piper/espeak-ng-data/lang/azc/nci new file mode 100644 index 0000000..f189637 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/azc/nci @@ -0,0 +1,6 @@ +name Nahuatl (Classical) +language nci + +intonation 3 +stressRule 2 +stressLength 190 190 200 200 0 0 220 240 diff --git a/tts/piper/espeak-ng-data/lang/bat/lt b/tts/piper/espeak-ng-data/lang/bat/lt new file mode 100644 index 0000000..7cdb193 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/bat/lt @@ -0,0 +1,2 @@ +name Lithuanian +language lt diff --git a/tts/piper/espeak-ng-data/lang/bat/ltg b/tts/piper/espeak-ng-data/lang/bat/ltg new file mode 100644 index 0000000..1041d9e --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/bat/ltg @@ -0,0 +1,12 @@ +name Latgalian +language ltg +maintainer Valdis Vitolins +status testing +phonemes lv +dictionary lv +dictrules 2 // Setting for Latgalian pronunciation +words 0 2 +pitch 64 118 +tone 60 150 204 100 400 255 700 10 3000 255 +stressAmp 12 10 8 8 0 0 15 16 +stressLength 160 140 200 140 0 0 240 160 diff --git a/tts/piper/espeak-ng-data/lang/bat/lv b/tts/piper/espeak-ng-data/lang/bat/lv new file mode 100644 index 0000000..70eb61d --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/bat/lv @@ -0,0 +1,9 @@ +name Latvian +language lv +maintainer Valdis Vitolins +status mature +words 0 2 +pitch 67 123 +tone 60 150 204 100 400 255 700 10 3000 255 +stressAmp 11 8 11 9 0 0 14 12 +stressLength 160 120 200 130 0 0 230 180 diff --git a/tts/piper/espeak-ng-data/lang/bnt/sw b/tts/piper/espeak-ng-data/lang/bnt/sw new file mode 100644 index 0000000..d1a4db3 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/bnt/sw @@ -0,0 +1,4 @@ +name Swahili +language sw + +status testing diff --git a/tts/piper/espeak-ng-data/lang/bnt/tn b/tts/piper/espeak-ng-data/lang/bnt/tn new file mode 100644 index 0000000..8b484e4 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/bnt/tn @@ -0,0 +1,4 @@ +name Setswana +language tn + +status testing diff --git a/tts/piper/espeak-ng-data/lang/ccs/ka b/tts/piper/espeak-ng-data/lang/ccs/ka new file mode 100644 index 0000000..2a789b3 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/ccs/ka @@ -0,0 +1,3 @@ +name Georgian +language ka +lowercaseSentence // A period followed by a lowercase letter is considered a sentence (mkhedruli) diff --git a/tts/piper/espeak-ng-data/lang/cel/cy b/tts/piper/espeak-ng-data/lang/cel/cy new file mode 100644 index 0000000..6d02132 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/cel/cy @@ -0,0 +1,4 @@ +name Welsh +language cy + +intonation 4 diff --git a/tts/piper/espeak-ng-data/lang/cel/ga b/tts/piper/espeak-ng-data/lang/cel/ga new file mode 100644 index 0000000..9fea2ae --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/cel/ga @@ -0,0 +1,4 @@ +name Gaelic (Irish) +language ga + +dictrules 1 // fix for eclipsis diff --git a/tts/piper/espeak-ng-data/lang/cel/gd b/tts/piper/espeak-ng-data/lang/cel/gd new file mode 100644 index 0000000..e416f6f --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/cel/gd @@ -0,0 +1,4 @@ +name Gaelic (Scottish) +language gd + +status testing diff --git a/tts/piper/espeak-ng-data/lang/cus/om b/tts/piper/espeak-ng-data/lang/cus/om new file mode 100644 index 0000000..1d6b396 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/cus/om @@ -0,0 +1,4 @@ +name Oromo +language om + +status testing diff --git a/tts/piper/espeak-ng-data/lang/dra/kn b/tts/piper/espeak-ng-data/lang/dra/kn new file mode 100644 index 0000000..a32732c --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/dra/kn @@ -0,0 +1,5 @@ +name Kannada +language kn + +intonation 2 +//consonants 80 diff --git a/tts/piper/espeak-ng-data/lang/dra/ml b/tts/piper/espeak-ng-data/lang/dra/ml new file mode 100644 index 0000000..92394bb --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/dra/ml @@ -0,0 +1,5 @@ +name Malayalam +language ml + +intonation 2 +//consonants 80 diff --git a/tts/piper/espeak-ng-data/lang/dra/ta b/tts/piper/espeak-ng-data/lang/dra/ta new file mode 100644 index 0000000..aae6334 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/dra/ta @@ -0,0 +1,5 @@ +name Tamil +language ta + +intonation 2 +consonants 80 diff --git a/tts/piper/espeak-ng-data/lang/dra/te b/tts/piper/espeak-ng-data/lang/dra/te new file mode 100644 index 0000000..8acbb18 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/dra/te @@ -0,0 +1,7 @@ +name Telugu +language te + +status testing + +intonation 2 +//consonants 80 diff --git a/tts/piper/espeak-ng-data/lang/esx/kl b/tts/piper/espeak-ng-data/lang/esx/kl new file mode 100644 index 0000000..e581b58 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/esx/kl @@ -0,0 +1,3 @@ +name Greenlandic +language kl + diff --git a/tts/piper/espeak-ng-data/lang/eu b/tts/piper/espeak-ng-data/lang/eu new file mode 100644 index 0000000..ef132e5 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/eu @@ -0,0 +1,5 @@ +name Basque +language eu + +status testing +stressRule 15 diff --git a/tts/piper/espeak-ng-data/lang/gmq/da b/tts/piper/espeak-ng-data/lang/gmq/da new file mode 100644 index 0000000..58f02f1 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmq/da @@ -0,0 +1,4 @@ +name Danish +language da + +tunes s2 c2 q2 e2 diff --git a/tts/piper/espeak-ng-data/lang/gmq/is b/tts/piper/espeak-ng-data/lang/gmq/is new file mode 100644 index 0000000..04bf5ad --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmq/is @@ -0,0 +1,2 @@ +name Icelandic +language is diff --git a/tts/piper/espeak-ng-data/lang/gmq/nb b/tts/piper/espeak-ng-data/lang/gmq/nb new file mode 100644 index 0000000..c29117f --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmq/nb @@ -0,0 +1,7 @@ +name Norwegian Bokmål +language nb +language no +phonemes no +dictionary no + +intonation 4 diff --git a/tts/piper/espeak-ng-data/lang/gmq/sv b/tts/piper/espeak-ng-data/lang/gmq/sv new file mode 100644 index 0000000..bb2d029 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmq/sv @@ -0,0 +1,2 @@ +name Swedish +language sv diff --git a/tts/piper/espeak-ng-data/lang/gmw/af b/tts/piper/espeak-ng-data/lang/gmw/af new file mode 100644 index 0000000..64fc96f --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmw/af @@ -0,0 +1,8 @@ +name Afrikaans +language af + +maintainer Christo de Klerk +status mature + +roughness 0 +pitch 63 120 diff --git a/tts/piper/espeak-ng-data/lang/gmw/de b/tts/piper/espeak-ng-data/lang/gmw/de new file mode 100644 index 0000000..a43cc1a --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmw/de @@ -0,0 +1,3 @@ +name German +language de +tunes s4 c1 q4 e1 diff --git a/tts/piper/espeak-ng-data/lang/gmw/en b/tts/piper/espeak-ng-data/lang/gmw/en new file mode 100644 index 0000000..f23fb53 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmw/en @@ -0,0 +1,8 @@ +name English (Great Britain) +language en-gb 2 +language en 2 + +maintainer Reece H. Dunn +status mature + +tunes s1 c1 q1 e1 diff --git a/tts/piper/espeak-ng-data/lang/gmw/en-029 b/tts/piper/espeak-ng-data/lang/gmw/en-029 new file mode 100644 index 0000000..493aae4 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmw/en-029 @@ -0,0 +1,20 @@ +name English (Caribbean) +language en-029 +language en 10 + +maintainer Reece H. Dunn +status mature + +phonemes en-wi +dictrules 8 +stressLength 175 175 175 175 220 220 250 290 + +replace 00 D d +replace 00 T t[ +replace 00 U@ o@ +replace 03 @ a# +replace 03 3 a# +replace 03 N n + +formant 1 98 100 100 +formant 2 98 100 100 diff --git a/tts/piper/espeak-ng-data/lang/gmw/en-GB-scotland b/tts/piper/espeak-ng-data/lang/gmw/en-GB-scotland new file mode 100644 index 0000000..a4655a0 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmw/en-GB-scotland @@ -0,0 +1,17 @@ +name English (Scotland) +language en-gb-scotland +language en 4 + +maintainer Reece H. Dunn +status mature + +phonemes en-sc +dictrules 2 5 6 7 +stressLength 180 130 200 200 0 0 250 270 + +replace 03 @ V +replace 03 I i +replace 03 I2 i +replace 01 aI aI2 +replace 02 a a/ +replace 02 u: U diff --git a/tts/piper/espeak-ng-data/lang/gmw/en-GB-x-gbclan b/tts/piper/espeak-ng-data/lang/gmw/en-GB-x-gbclan new file mode 100644 index 0000000..f54a06b --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmw/en-GB-x-gbclan @@ -0,0 +1,14 @@ +name English (Lancaster) +language en-gb-x-gbclan +language en-gb 3 +language en 5 + +maintainer Reece H. Dunn +status mature + +phonemes en-n + +stressLength 160 150 180 180 220 220 290 290 + +replace 03 N n +replace 03 i I2 diff --git a/tts/piper/espeak-ng-data/lang/gmw/en-GB-x-gbcwmd b/tts/piper/espeak-ng-data/lang/gmw/en-GB-x-gbcwmd new file mode 100644 index 0000000..1831f9a --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmw/en-GB-x-gbcwmd @@ -0,0 +1,12 @@ +name English (West Midlands) +language en-gb-x-gbcwmd +language en-gb 9 +language en 9 + +phonemes en-wm + +replace 00 h NULL +replace 00 o@ O@ +dictrules 6 +intonation 4 +stressAdd 0 0 0 0 0 0 0 20 diff --git a/tts/piper/espeak-ng-data/lang/gmw/en-GB-x-rp b/tts/piper/espeak-ng-data/lang/gmw/en-GB-x-rp new file mode 100644 index 0000000..fb72cf1 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmw/en-GB-x-rp @@ -0,0 +1,15 @@ +name English (Received Pronunciation) +language en-gb-x-rp +language en-gb 4 +language en 5 + +maintainer Reece H. Dunn +status mature + +phonemes en-rp + +replace 00 o@ O@ +replace 03 I i +replace 03 I2 i +replace 03 @ a# +replace 03 3 a# diff --git a/tts/piper/espeak-ng-data/lang/gmw/en-US b/tts/piper/espeak-ng-data/lang/gmw/en-US new file mode 100644 index 0000000..31db652 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmw/en-US @@ -0,0 +1,15 @@ +name English (America) +language en-us 2 +language en 3 + +maintainer Reece H. Dunn +status mature + +phonemes en-us +dictrules 3 6 + +stressLength 140 120 190 170 0 0 255 300 +stressAmp 17 16 19 19 19 19 21 19 + +replace 03 I i +replace 03 I2 i diff --git a/tts/piper/espeak-ng-data/lang/gmw/en-US-nyc b/tts/piper/espeak-ng-data/lang/gmw/en-US-nyc new file mode 100644 index 0000000..2d76f88 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmw/en-US-nyc @@ -0,0 +1,14 @@ +name English (America, New York City) +language en-us-nyc + +maintainer Richard Calvi +status testing + +phonemes en-us-nyc +dictrules 3 6 + +stressLength 140 120 190 170 0 0 255 300 +stressAmp 17 16 19 19 19 19 21 19 + +replace 03 I i +replace 03 I2 i diff --git a/tts/piper/espeak-ng-data/lang/gmw/lb b/tts/piper/espeak-ng-data/lang/gmw/lb new file mode 100644 index 0000000..7972459 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmw/lb @@ -0,0 +1,2 @@ +name Luxembourgish +language lb diff --git a/tts/piper/espeak-ng-data/lang/gmw/nl b/tts/piper/espeak-ng-data/lang/gmw/nl new file mode 100644 index 0000000..a212fbe --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/gmw/nl @@ -0,0 +1,2 @@ +name Dutch +language nl diff --git a/tts/piper/espeak-ng-data/lang/grk/el b/tts/piper/espeak-ng-data/lang/grk/el new file mode 100644 index 0000000..548a01b --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/grk/el @@ -0,0 +1,2 @@ +name Greek +language el diff --git a/tts/piper/espeak-ng-data/lang/grk/grc b/tts/piper/espeak-ng-data/lang/grk/grc new file mode 100644 index 0000000..baa8b2c --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/grk/grc @@ -0,0 +1,6 @@ +name Greek (Ancient) +language grc + +stressLength 170 170 190 190 0 0 230 240 +dictrules 1 +words 3 diff --git a/tts/piper/espeak-ng-data/lang/inc/as b/tts/piper/espeak-ng-data/lang/inc/as new file mode 100644 index 0000000..23991ad --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/inc/as @@ -0,0 +1,4 @@ +name Assamese +language as + +status testing diff --git a/tts/piper/espeak-ng-data/lang/inc/bn b/tts/piper/espeak-ng-data/lang/inc/bn new file mode 100644 index 0000000..c791802 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/inc/bn @@ -0,0 +1,2 @@ +name Bengali +language bn diff --git a/tts/piper/espeak-ng-data/lang/inc/bpy b/tts/piper/espeak-ng-data/lang/inc/bpy new file mode 100644 index 0000000..b63bc8d --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/inc/bpy @@ -0,0 +1,2 @@ +name Bishnupriya Manipuri +language bpy diff --git a/tts/piper/espeak-ng-data/lang/inc/gu b/tts/piper/espeak-ng-data/lang/inc/gu new file mode 100644 index 0000000..fcdacd2 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/inc/gu @@ -0,0 +1,4 @@ +name Gujarati +language gu + +status testing diff --git a/tts/piper/espeak-ng-data/lang/inc/hi b/tts/piper/espeak-ng-data/lang/inc/hi new file mode 100644 index 0000000..16ea389 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/inc/hi @@ -0,0 +1,2 @@ +name Hindi +language hi diff --git a/tts/piper/espeak-ng-data/lang/inc/kok b/tts/piper/espeak-ng-data/lang/inc/kok new file mode 100644 index 0000000..f2b1cc5 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/inc/kok @@ -0,0 +1,2 @@ +name Konkani +language kok diff --git a/tts/piper/espeak-ng-data/lang/inc/mr b/tts/piper/espeak-ng-data/lang/inc/mr new file mode 100644 index 0000000..d9181f9 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/inc/mr @@ -0,0 +1,4 @@ +name Marathi +language mr + +status testing diff --git a/tts/piper/espeak-ng-data/lang/inc/ne b/tts/piper/espeak-ng-data/lang/inc/ne new file mode 100644 index 0000000..4b64cc2 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/inc/ne @@ -0,0 +1,4 @@ +name Nepali +language ne + +dictrules 1 diff --git a/tts/piper/espeak-ng-data/lang/inc/or b/tts/piper/espeak-ng-data/lang/inc/or new file mode 100644 index 0000000..95a990e --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/inc/or @@ -0,0 +1,4 @@ +name Oriya +language or + +status testing diff --git a/tts/piper/espeak-ng-data/lang/inc/pa b/tts/piper/espeak-ng-data/lang/inc/pa new file mode 100644 index 0000000..0e9552d --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/inc/pa @@ -0,0 +1,2 @@ +name Punjabi +language pa diff --git a/tts/piper/espeak-ng-data/lang/inc/sd b/tts/piper/espeak-ng-data/lang/inc/sd new file mode 100644 index 0000000..51cf8af --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/inc/sd @@ -0,0 +1,3 @@ +name Sindhi +language sd +maintainer Ejaz Shah diff --git a/tts/piper/espeak-ng-data/lang/inc/si b/tts/piper/espeak-ng-data/lang/inc/si new file mode 100644 index 0000000..3f87719 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/inc/si @@ -0,0 +1,6 @@ +name Sinhala +language si + +status testing + +intonation 2 diff --git a/tts/piper/espeak-ng-data/lang/inc/ur b/tts/piper/espeak-ng-data/lang/inc/ur new file mode 100644 index 0000000..16e5469 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/inc/ur @@ -0,0 +1,7 @@ +name Urdu +language ur +maintainer Ejaz Shah +status testing + +stressRule 6 + diff --git a/tts/piper/espeak-ng-data/lang/ine/hy b/tts/piper/espeak-ng-data/lang/ine/hy new file mode 100644 index 0000000..ee0be2f --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/ine/hy @@ -0,0 +1,3 @@ +name Armenian (East Armenia) +language hy +language hy-arevela diff --git a/tts/piper/espeak-ng-data/lang/ine/hyw b/tts/piper/espeak-ng-data/lang/ine/hyw new file mode 100644 index 0000000..9d5fe86 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/ine/hyw @@ -0,0 +1,24 @@ +name Armenian (West Armenia) +language hyw +language hy-arevmda +language hy 8 + +dictionary hy +dictrules 1 + +phonemes hy + +// change consonants for West Armenian pronunciation +replace 00 b p# +replace 00 d t# +replace 00 dz ts# +replace 00 dZ tS# +replace 00 g k# + +replace 00 p b +replace 00 t d +replace 00 ts dz +replace 00 tS dZ +replace 00 k g + +replace 00 R2 R // ?? diff --git a/tts/piper/espeak-ng-data/lang/ine/sq b/tts/piper/espeak-ng-data/lang/ine/sq new file mode 100644 index 0000000..9ace357 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/ine/sq @@ -0,0 +1,5 @@ +name Albanian +language sq + +// add this line to remove 'ë' at the end of words +// replace 00 @/ NULL diff --git a/tts/piper/espeak-ng-data/lang/ira/fa b/tts/piper/espeak-ng-data/lang/ira/fa new file mode 100644 index 0000000..5a0c2b6 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/ira/fa @@ -0,0 +1,4 @@ +name Persian +language fa +maintainer Shadyar Khodayari +status mature diff --git a/tts/piper/espeak-ng-data/lang/ira/fa-Latn b/tts/piper/espeak-ng-data/lang/ira/fa-Latn new file mode 100644 index 0000000..02c6da6 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/ira/fa-Latn @@ -0,0 +1,8 @@ +name Persian (Pinglish) +// Sometimes, Farsi speakers write Farsi words using English characters, particularly in Chat and SMS (texte messages).), called Pinglish +language fa-latn +maintainer Shadyar Khodayari +status mature +dictrules 1 +phonemes fa + diff --git a/tts/piper/espeak-ng-data/lang/ira/ku b/tts/piper/espeak-ng-data/lang/ira/ku new file mode 100644 index 0000000..b26bca2 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/ira/ku @@ -0,0 +1,5 @@ +name Kurdish +language ku + +//words 1 48 + diff --git a/tts/piper/espeak-ng-data/lang/iro/chr b/tts/piper/espeak-ng-data/lang/iro/chr new file mode 100644 index 0000000..abe2938 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/iro/chr @@ -0,0 +1,27 @@ +name Cherokee //https://github.com/espeak-ng/espeak-ng/blob/master/docs/voices.md +language chr-US-Qaaa-x-west 5 + +maintainer Michael Conrad +status testing + +pitch 90 160 + +voicing 100 + +consonants 100 100 + +speed 100 + +words 2 1 + +phonemes chr + +//stress on all syllables to simulate stress on no syllables +stressRule 9 +stressLength 175 175 175 175 175 175 175 175 //all vowels the same length regardless of stress +stressAmp 10 10 10 10 10 10 10 10 //all vowels the same strength regardless of marked stress + +intonation 1 + +tunes chrs chrc chrq chre + diff --git a/tts/piper/espeak-ng-data/lang/itc/la b/tts/piper/espeak-ng-data/lang/itc/la new file mode 100644 index 0000000..c57270f --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/itc/la @@ -0,0 +1,12 @@ +name Latin +language la +stressRule 2 0 2 +// rule=penultimate +// unstressed_wd1=0 +// unstressed_wd2=2 +stressOpt 0 5 // flags=0100001 (no automatic secondary stress + don't stres monosyllables) + +// short gap between words +words 2 + +// Note: The Latin voice needs long vowels to be marked with macrons diff --git a/tts/piper/espeak-ng-data/lang/jpx/ja b/tts/piper/espeak-ng-data/lang/jpx/ja new file mode 100644 index 0000000..20e21a1 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/jpx/ja @@ -0,0 +1,5 @@ +name Japanese +language ja +phonemes ja + +intonation 4 diff --git a/tts/piper/espeak-ng-data/lang/ko b/tts/piper/espeak-ng-data/lang/ko new file mode 100644 index 0000000..f03ba09 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/ko @@ -0,0 +1,5 @@ +name Korean +language ko +pitch 80 118 +intonation 2 + diff --git a/tts/piper/espeak-ng-data/lang/map/haw b/tts/piper/espeak-ng-data/lang/map/haw new file mode 100644 index 0000000..c086913 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/map/haw @@ -0,0 +1,3 @@ +name Hawaiian +language haw +status testing diff --git a/tts/piper/espeak-ng-data/lang/miz/mto b/tts/piper/espeak-ng-data/lang/miz/mto new file mode 100644 index 0000000..eb0f75f --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/miz/mto @@ -0,0 +1,8 @@ +name Totontepec Mixe +language mto + +maintainer Bill Dengler and Elizabeth Resendiz +status testing + +lowercaseSentence +tunes s6 c6 q6 e6 diff --git a/tts/piper/espeak-ng-data/lang/myn/quc b/tts/piper/espeak-ng-data/lang/myn/quc new file mode 100644 index 0000000..e8e0ec6 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/myn/quc @@ -0,0 +1,6 @@ +name K'iche' +language quc +status testing +stressRule 3 // stress on final syllable +stressAmp 8 8 20 15 0 0 25 25 // reduce unstressed vowels +stressLength 120 120 200 150 0 0 250 250 // reduce unstressed vowels diff --git a/tts/piper/espeak-ng-data/lang/poz/id b/tts/piper/espeak-ng-data/lang/poz/id new file mode 100644 index 0000000..d300372 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/poz/id @@ -0,0 +1,7 @@ +name Indonesian +language id + +stressLength 160 200 180 180 0 0 220 240 +stressAmp 16 18 18 18 0 0 22 21 + +consonants 80 80 diff --git a/tts/piper/espeak-ng-data/lang/poz/mi b/tts/piper/espeak-ng-data/lang/poz/mi new file mode 100644 index 0000000..fa1c71e --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/poz/mi @@ -0,0 +1,21 @@ +name Māori +language mi +status testing + +// https://github.com/espeak-ng/espeak-ng/blob/master/docs/voices.md#words +words 1 2 + +// taken from Jacky +pitch 115 130 + +formant 0 150 155 100 +formant 1 90 155 70 +formant 2 95 70 64 +formant 3 15 20 30 +formant 4 20 30 40 +formant 5 65 20 65 +formant 6 70 80 100 +formant 7 20 80 100 +formant 8 100 95 80 +voicing 135 +consonants 110 diff --git a/tts/piper/espeak-ng-data/lang/poz/ms b/tts/piper/espeak-ng-data/lang/poz/ms new file mode 100644 index 0000000..27a2712 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/poz/ms @@ -0,0 +1,14 @@ +// Last updated: 14 October 2010, Jason Ong (jason@portalgroove.com) +name Malay +language ms +phonemes id + +stressLength 160 200 180 180 0 0 220 240 +stressAmp 16 18 18 18 0 0 22 21 +intonation 3 // Less intonation, and comma does not raise the pitch. + +// Nuance - Peninsula Malaysia +// replace 3 a @ // change 'saya' to 'saye' + // (only the last phoneme of a word, only in unstressed syllables) + +consonants 80 80 diff --git a/tts/piper/espeak-ng-data/lang/qu b/tts/piper/espeak-ng-data/lang/qu new file mode 100644 index 0000000..aa717f2 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/qu @@ -0,0 +1,5 @@ +name Quechua +language qu +stressRule 2 // stress on penultimate syllable +status testing + diff --git a/tts/piper/espeak-ng-data/lang/roa/an b/tts/piper/espeak-ng-data/lang/roa/an new file mode 100644 index 0000000..dc75aa8 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/roa/an @@ -0,0 +1,2 @@ +name Aragonese +language an diff --git a/tts/piper/espeak-ng-data/lang/roa/ca b/tts/piper/espeak-ng-data/lang/roa/ca new file mode 100644 index 0000000..54af356 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/roa/ca @@ -0,0 +1,2 @@ +name Catalan +language ca diff --git a/tts/piper/espeak-ng-data/lang/roa/es b/tts/piper/espeak-ng-data/lang/roa/es new file mode 100644 index 0000000..dc90e41 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/roa/es @@ -0,0 +1,4 @@ +name Spanish (Spain) +language es +dictrules 1 +tunes s6 c6 q6 e6 diff --git a/tts/piper/espeak-ng-data/lang/roa/es-419 b/tts/piper/espeak-ng-data/lang/roa/es-419 new file mode 100644 index 0000000..236bef0 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/roa/es-419 @@ -0,0 +1,11 @@ +name Spanish (Latin America) +language es-419 +language es-mx 6 + +phonemes es-la +dictrules 2 +intonation 2 +stressLength 170 200 230 180 0 0 250 280 + +tunes s6 c6 q6 e6 + diff --git a/tts/piper/espeak-ng-data/lang/roa/fr b/tts/piper/espeak-ng-data/lang/roa/fr new file mode 100644 index 0000000..9e0b944 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/roa/fr @@ -0,0 +1,6 @@ +name French (France) +language fr-fr +language fr + +dictrules 1 +tunes s3 c3 q3 e3 diff --git a/tts/piper/espeak-ng-data/lang/roa/fr-BE b/tts/piper/espeak-ng-data/lang/roa/fr-BE new file mode 100644 index 0000000..1a0311f --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/roa/fr-BE @@ -0,0 +1,8 @@ +name French (Belgium) +language fr-be +language fr 8 + +dictrules 2 +tunes s3 c3 q3 e3 + + diff --git a/tts/piper/espeak-ng-data/lang/roa/fr-CH b/tts/piper/espeak-ng-data/lang/roa/fr-CH new file mode 100644 index 0000000..2e45fbd --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/roa/fr-CH @@ -0,0 +1,6 @@ +name French (Switzerland) +language fr-ch +language fr 8 + +dictrules 3 +tunes s3 c3 q3 e3 diff --git a/tts/piper/espeak-ng-data/lang/roa/ht b/tts/piper/espeak-ng-data/lang/roa/ht new file mode 100644 index 0000000..8b57225 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/roa/ht @@ -0,0 +1,8 @@ +name Haitian Creole +language ht +status testing +maintainer // TODO somebody should take responsibility for this + +phonemes ht +dictionary ht + diff --git a/tts/piper/espeak-ng-data/lang/roa/it b/tts/piper/espeak-ng-data/lang/roa/it new file mode 100644 index 0000000..ad7bb50 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/roa/it @@ -0,0 +1,7 @@ +name Italian +language it + +maintainer Christian Leo M +status mature + +tunes s4 c4 q4 e4 diff --git a/tts/piper/espeak-ng-data/lang/roa/pap b/tts/piper/espeak-ng-data/lang/roa/pap new file mode 100644 index 0000000..b9e07fe --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/roa/pap @@ -0,0 +1,7 @@ +name Papiamento +language pap + +status testing + +phonemes base2 + diff --git a/tts/piper/espeak-ng-data/lang/roa/pt b/tts/piper/espeak-ng-data/lang/roa/pt new file mode 100644 index 0000000..5041b51 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/roa/pt @@ -0,0 +1,7 @@ +name Portuguese (Portugal) +language pt +language pt-pt +phonemes pt-pt + +dictrules 1 +intonation 2 diff --git a/tts/piper/espeak-ng-data/lang/roa/pt-BR b/tts/piper/espeak-ng-data/lang/roa/pt-BR new file mode 100644 index 0000000..52b850c --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/roa/pt-BR @@ -0,0 +1,7 @@ +name Portuguese (Brazil) +language pt-br +language pt 6 + +dictrules 2 +stressLength 200 115 230 230 0 0 250 270 + diff --git a/tts/piper/espeak-ng-data/lang/roa/ro b/tts/piper/espeak-ng-data/lang/roa/ro new file mode 100644 index 0000000..94441ef --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/roa/ro @@ -0,0 +1,2 @@ +name Romanian +language ro diff --git a/tts/piper/espeak-ng-data/lang/sai/gn b/tts/piper/espeak-ng-data/lang/sai/gn new file mode 100644 index 0000000..6e183e5 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/sai/gn @@ -0,0 +1,4 @@ +name Guarani +language gn +dictrules 1 +words 0 1 diff --git a/tts/piper/espeak-ng-data/lang/sem/am b/tts/piper/espeak-ng-data/lang/sem/am new file mode 100644 index 0000000..87ed65a --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/sem/am @@ -0,0 +1,4 @@ +name Amharic +language am + +status testing diff --git a/tts/piper/espeak-ng-data/lang/sem/ar b/tts/piper/espeak-ng-data/lang/sem/ar new file mode 100644 index 0000000..5219808 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/sem/ar @@ -0,0 +1,5 @@ +name Arabic +language ar +phonemes ar + +stressRule 4 diff --git a/tts/piper/espeak-ng-data/lang/sem/he b/tts/piper/espeak-ng-data/lang/sem/he new file mode 100644 index 0000000..38cce90 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/sem/he @@ -0,0 +1,4 @@ +name Hebrew +language he + +status testing diff --git a/tts/piper/espeak-ng-data/lang/sem/mt b/tts/piper/espeak-ng-data/lang/sem/mt new file mode 100644 index 0000000..ea8a3fc --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/sem/mt @@ -0,0 +1,4 @@ +name Maltese +language mt + +status testing diff --git a/tts/piper/espeak-ng-data/lang/sit/cmn b/tts/piper/espeak-ng-data/lang/sit/cmn new file mode 100644 index 0000000..451016d --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/sit/cmn @@ -0,0 +1,36 @@ +name Chinese (Mandarin, latin as English) +language cmn +language zh-cmn +language zh + +phonemes cmn +dictionary cmn +words 1 +pitch 80 118 + +dict_min 100000 + +//for some dialects + +//[en]: replace ng with n +//[zh]: �޺�������ng���n +//replace 0 N n + +//[en]: replace rfx consonants +//[zh]: �޾�������r���l��z��er���e +//replace 0 ts.h tsh +//replace 0 ts. ts +//replace 0 s. s +//replace 0 i. i[ +//replace 0 z. l +//replace 0 z. z +//replace 0 @r @ + +//[en]: replace beginning n or l +//[zh]: ����nl��n���l��l���n +//replace 2 n l +//replace 2 l n + +//[en]: replace beginning w with v +//[zh]: w���v +//replace 0 w v diff --git a/tts/piper/espeak-ng-data/lang/sit/cmn-Latn-pinyin b/tts/piper/espeak-ng-data/lang/sit/cmn-Latn-pinyin new file mode 100644 index 0000000..4b63db7 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/sit/cmn-Latn-pinyin @@ -0,0 +1,11 @@ +name Chinese (Mandarin, latin as Pinyin) +language cmn-latn-pinyin +language zh-cmn +language zh + +phonemes cmn +dictionary cmn +words 1 +pitch 80 118 + +dict_min 100000 diff --git a/tts/piper/espeak-ng-data/lang/sit/hak b/tts/piper/espeak-ng-data/lang/sit/hak new file mode 100644 index 0000000..0c3bf14 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/sit/hak @@ -0,0 +1,6 @@ +name Hakka Chinese +language hak +maintainer Chen Chien-ting +status testing +phonemes hak +dictionary hak diff --git a/tts/piper/espeak-ng-data/lang/sit/my b/tts/piper/espeak-ng-data/lang/sit/my new file mode 100644 index 0000000..3dbc767 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/sit/my @@ -0,0 +1,3 @@ +name Myanmar (Burmese) +maintainer Min Maung +language my diff --git a/tts/piper/espeak-ng-data/lang/sit/yue b/tts/piper/espeak-ng-data/lang/sit/yue new file mode 100644 index 0000000..d65fac1 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/sit/yue @@ -0,0 +1,13 @@ +name Chinese (Cantonese) +language yue +language zh-yue +language zh 8 + +phonemes yue +dictionary yue + +// interpret English letters as 1=English words, 2=jyutping +dictrules 1 + +words 1 +dict_min 10000 diff --git a/tts/piper/espeak-ng-data/lang/sit/yue-Latn-jyutping b/tts/piper/espeak-ng-data/lang/sit/yue-Latn-jyutping new file mode 100644 index 0000000..2324cd7 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/sit/yue-Latn-jyutping @@ -0,0 +1,13 @@ +name Chinese (Cantonese, latin as Jyutping) +language yue +language zh-yue +language zh 8 + +phonemes yue +dictionary yue + +// interpret English letters as 1=English words, 2=jyutping +dictrules 2 + +words 1 +dict_min 10000 diff --git a/tts/piper/espeak-ng-data/lang/tai/shn b/tts/piper/espeak-ng-data/lang/tai/shn new file mode 100644 index 0000000..8d30384 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/tai/shn @@ -0,0 +1,4 @@ +name Shan (Tai Yai) +language shn +maintainer ronaldaug +status testing diff --git a/tts/piper/espeak-ng-data/lang/tai/th b/tts/piper/espeak-ng-data/lang/tai/th new file mode 100644 index 0000000..9b337c1 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/tai/th @@ -0,0 +1,3 @@ +name Thai +language th +status testing diff --git a/tts/piper/espeak-ng-data/lang/trk/az b/tts/piper/espeak-ng-data/lang/trk/az new file mode 100644 index 0000000..fd9e4f3 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/trk/az @@ -0,0 +1,4 @@ +name Azerbaijani +language az + +status testing diff --git a/tts/piper/espeak-ng-data/lang/trk/ba b/tts/piper/espeak-ng-data/lang/trk/ba new file mode 100644 index 0000000..09fab48 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/trk/ba @@ -0,0 +1,2 @@ +name Bashkir +language ba diff --git a/tts/piper/espeak-ng-data/lang/trk/cv b/tts/piper/espeak-ng-data/lang/trk/cv new file mode 100644 index 0000000..44e37f6 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/trk/cv @@ -0,0 +1,3 @@ +name Chuvash +language cv +status testing diff --git a/tts/piper/espeak-ng-data/lang/trk/kk b/tts/piper/espeak-ng-data/lang/trk/kk new file mode 100644 index 0000000..df3b0a0 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/trk/kk @@ -0,0 +1,4 @@ +name Kazakh +language kk +status testing + diff --git a/tts/piper/espeak-ng-data/lang/trk/ky b/tts/piper/espeak-ng-data/lang/trk/ky new file mode 100644 index 0000000..c031449 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/trk/ky @@ -0,0 +1,4 @@ +name Kyrgyz +language ky + +tunes s3 c3 q3 e3 diff --git a/tts/piper/espeak-ng-data/lang/trk/nog b/tts/piper/espeak-ng-data/lang/trk/nog new file mode 100644 index 0000000..691d031 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/trk/nog @@ -0,0 +1,3 @@ +name Nogai +language nog +status testing diff --git a/tts/piper/espeak-ng-data/lang/trk/tk b/tts/piper/espeak-ng-data/lang/trk/tk new file mode 100644 index 0000000..795413a --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/trk/tk @@ -0,0 +1,2 @@ +name Turkmen +language tk diff --git a/tts/piper/espeak-ng-data/lang/trk/tr b/tts/piper/espeak-ng-data/lang/trk/tr new file mode 100644 index 0000000..1d5cbc4 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/trk/tr @@ -0,0 +1,2 @@ +name Turkish +language tr diff --git a/tts/piper/espeak-ng-data/lang/trk/tt b/tts/piper/espeak-ng-data/lang/trk/tt new file mode 100644 index 0000000..10ebba4 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/trk/tt @@ -0,0 +1,2 @@ +name Tatar +language tt diff --git a/tts/piper/espeak-ng-data/lang/trk/ug b/tts/piper/espeak-ng-data/lang/trk/ug new file mode 100644 index 0000000..f81b44f --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/trk/ug @@ -0,0 +1,2 @@ +name Uyghur +language ug diff --git a/tts/piper/espeak-ng-data/lang/trk/uz b/tts/piper/espeak-ng-data/lang/trk/uz new file mode 100644 index 0000000..b1a7586 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/trk/uz @@ -0,0 +1,4 @@ +name Uzbek +language uz + +status testing diff --git a/tts/piper/espeak-ng-data/lang/urj/et b/tts/piper/espeak-ng-data/lang/urj/et new file mode 100644 index 0000000..096565f --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/urj/et @@ -0,0 +1,10 @@ +name Estonian +language et + +stressAmp 18 16 22 22 20 22 22 22 +stressLength 150 180 200 200 0 0 210 250 +stressOpt 1 2 4 6 // (S_NO_DIM + S_FINAL_DIM = S_FINAL_DIM_ONLY), S_FINAL_NO_2, S_2_TO_HEAVY +stressRule 0 + +intonation 3 +spellingStress diff --git a/tts/piper/espeak-ng-data/lang/urj/fi b/tts/piper/espeak-ng-data/lang/urj/fi new file mode 100644 index 0000000..fe2447c --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/urj/fi @@ -0,0 +1,11 @@ +name Finnish +language fi + + +stressAmp 18 16 22 22 20 22 22 22 +stressLength 150 180 200 200 0 0 210 250 +stressOpt 1 2 4 6 // (S_NO_DIM + S_FINAL_DIM = S_FINAL_DIM_ONLY), S_FINAL_NO_2, S_2_TO_HEAVY +stressRule 0 + +intonation 3 +spellingStress diff --git a/tts/piper/espeak-ng-data/lang/urj/hu b/tts/piper/espeak-ng-data/lang/urj/hu new file mode 100644 index 0000000..f5bb295 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/urj/hu @@ -0,0 +1,7 @@ +name Hungarian +language hu +brackets 0 +bracketsAnnounced 0 +pitch 81 117 + + diff --git a/tts/piper/espeak-ng-data/lang/urj/smj b/tts/piper/espeak-ng-data/lang/urj/smj new file mode 100644 index 0000000..316559c --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/urj/smj @@ -0,0 +1,4 @@ +name Lule Saami +language smj + +status testing diff --git a/tts/piper/espeak-ng-data/lang/zle/be b/tts/piper/espeak-ng-data/lang/zle/be new file mode 100644 index 0000000..31b14e5 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/zle/be @@ -0,0 +1,4 @@ +name Belarusian +language be +dict_min 2000 +speed 95 diff --git a/tts/piper/espeak-ng-data/lang/zle/ru b/tts/piper/espeak-ng-data/lang/zle/ru new file mode 100644 index 0000000..959754b --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/zle/ru @@ -0,0 +1,4 @@ +name Russian +language ru +replace 03 a a# +dict_min 20000 diff --git a/tts/piper/espeak-ng-data/lang/zle/ru-LV b/tts/piper/espeak-ng-data/lang/zle/ru-LV new file mode 100644 index 0000000..fc9541b --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/zle/ru-LV @@ -0,0 +1,16 @@ +name Russian (Latvia) +language ru-lv 2 + +maintainer Valdis Vitolins +status testing + +phonemes ru-lv +dictrules 2 +dict_min 20000 +speed 95 + +words 0 2 +tone 150 220 450 255 750 20 3500 255 +stressAmp 12 10 8 8 0 0 16 17 +stressLength 160 140 200 140 0 0 240 160 + diff --git a/tts/piper/espeak-ng-data/lang/zle/ru-cl b/tts/piper/espeak-ng-data/lang/zle/ru-cl new file mode 100644 index 0000000..ad100ca --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/zle/ru-cl @@ -0,0 +1,6 @@ +name Russian (Classic) +language ru-cl +replace 03 a a# +dict_min 20000 +speed 95 +dictrules 3 diff --git a/tts/piper/espeak-ng-data/lang/zle/uk b/tts/piper/espeak-ng-data/lang/zle/uk new file mode 100644 index 0000000..b318c23 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/zle/uk @@ -0,0 +1,7 @@ +name Ukrainian +language uk + +maintainer Andrij Mizyk +status testing + +speed 80 \ No newline at end of file diff --git a/tts/piper/espeak-ng-data/lang/zls/bg b/tts/piper/espeak-ng-data/lang/zls/bg new file mode 100644 index 0000000..870c042 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/zls/bg @@ -0,0 +1,5 @@ +name Bulgarian +language bg + +stressAmp 13 12 17 17 20 22 22 21 +stressLength 180 170 200 200 200 200 210 220 diff --git a/tts/piper/espeak-ng-data/lang/zls/bs b/tts/piper/espeak-ng-data/lang/zls/bs new file mode 100644 index 0000000..01fd174 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/zls/bs @@ -0,0 +1,14 @@ +name Bosnian +language bs +phonemes hr + +pitch 81 120 +formant 0 100 100 100 +formant 1 97 97 100 +formant 2 97 97 100 +formant 3 97 102 100 +formant 4 97 102 100 +formant 5 97 102 100 + +stressAdd 10 10 0 0 0 0 -30 -30 +dictrules 3 4 diff --git a/tts/piper/espeak-ng-data/lang/zls/hr b/tts/piper/espeak-ng-data/lang/zls/hr new file mode 100644 index 0000000..d1cae3a --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/zls/hr @@ -0,0 +1,15 @@ +name Croatian +language hr +language hbs + +// attributes towards !variant3 +pitch 81 120 +formant 0 100 100 100 +formant 1 97 97 100 +formant 2 97 97 100 +formant 3 97 102 100 +formant 4 97 102 100 +formant 5 97 102 100 + +stressAdd 10 10 0 0 0 0 -30 -30 +dictrules 1 diff --git a/tts/piper/espeak-ng-data/lang/zls/mk b/tts/piper/espeak-ng-data/lang/zls/mk new file mode 100644 index 0000000..380c20b --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/zls/mk @@ -0,0 +1,2 @@ +name Macedonian +language mk diff --git a/tts/piper/espeak-ng-data/lang/zls/sl b/tts/piper/espeak-ng-data/lang/zls/sl new file mode 100644 index 0000000..b7bf01f --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/zls/sl @@ -0,0 +1,4 @@ +name Slovenian +language sl + +status testing diff --git a/tts/piper/espeak-ng-data/lang/zls/sr b/tts/piper/espeak-ng-data/lang/zls/sr new file mode 100644 index 0000000..81e3390 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/zls/sr @@ -0,0 +1,13 @@ +name Serbian +language sr + +// attributes towards !variant3 pitch 80 120 +formant 0 100 100 100 +formant 1 97 97 100 +formant 2 97 97 100 +formant 3 97 102 100 +formant 4 97 102 100 +formant 5 97 102 100 + +stressAdd 10 10 0 0 0 0 -30 -30 +dictrules 2 4 diff --git a/tts/piper/espeak-ng-data/lang/zlw/cs b/tts/piper/espeak-ng-data/lang/zlw/cs new file mode 100644 index 0000000..5c3bf2a --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/zlw/cs @@ -0,0 +1,2 @@ +name Czech +language cs diff --git a/tts/piper/espeak-ng-data/lang/zlw/pl b/tts/piper/espeak-ng-data/lang/zlw/pl new file mode 100644 index 0000000..b85f56b --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/zlw/pl @@ -0,0 +1,4 @@ +name Polish +language pl + +intonation 2 diff --git a/tts/piper/espeak-ng-data/lang/zlw/sk b/tts/piper/espeak-ng-data/lang/zlw/sk new file mode 100644 index 0000000..e849e61 --- /dev/null +++ b/tts/piper/espeak-ng-data/lang/zlw/sk @@ -0,0 +1,2 @@ +name Slovak +language sk diff --git a/tts/piper/espeak-ng-data/lb_dict b/tts/piper/espeak-ng-data/lb_dict new file mode 100644 index 0000000..d0aee67 Binary files /dev/null and b/tts/piper/espeak-ng-data/lb_dict differ diff --git a/tts/piper/espeak-ng-data/lfn_dict b/tts/piper/espeak-ng-data/lfn_dict new file mode 100644 index 0000000..9a8d4f8 Binary files /dev/null and b/tts/piper/espeak-ng-data/lfn_dict differ diff --git a/tts/piper/espeak-ng-data/lt_dict b/tts/piper/espeak-ng-data/lt_dict new file mode 100644 index 0000000..d0dd29f Binary files /dev/null and b/tts/piper/espeak-ng-data/lt_dict differ diff --git a/tts/piper/espeak-ng-data/lv_dict b/tts/piper/espeak-ng-data/lv_dict new file mode 100644 index 0000000..60d70b1 Binary files /dev/null and b/tts/piper/espeak-ng-data/lv_dict differ diff --git a/tts/piper/espeak-ng-data/mi_dict b/tts/piper/espeak-ng-data/mi_dict new file mode 100644 index 0000000..b727a30 Binary files /dev/null and b/tts/piper/espeak-ng-data/mi_dict differ diff --git a/tts/piper/espeak-ng-data/mk_dict b/tts/piper/espeak-ng-data/mk_dict new file mode 100644 index 0000000..974626c Binary files /dev/null and b/tts/piper/espeak-ng-data/mk_dict differ diff --git a/tts/piper/espeak-ng-data/ml_dict b/tts/piper/espeak-ng-data/ml_dict new file mode 100644 index 0000000..e12f222 Binary files /dev/null and b/tts/piper/espeak-ng-data/ml_dict differ diff --git a/tts/piper/espeak-ng-data/mr_dict b/tts/piper/espeak-ng-data/mr_dict new file mode 100644 index 0000000..2cbf14d Binary files /dev/null and b/tts/piper/espeak-ng-data/mr_dict differ diff --git a/tts/piper/espeak-ng-data/ms_dict b/tts/piper/espeak-ng-data/ms_dict new file mode 100644 index 0000000..cb6fc29 Binary files /dev/null and b/tts/piper/espeak-ng-data/ms_dict differ diff --git a/tts/piper/espeak-ng-data/mt_dict b/tts/piper/espeak-ng-data/mt_dict new file mode 100644 index 0000000..8cc965b Binary files /dev/null and b/tts/piper/espeak-ng-data/mt_dict differ diff --git a/tts/piper/espeak-ng-data/mto_dict b/tts/piper/espeak-ng-data/mto_dict new file mode 100644 index 0000000..280aaa4 Binary files /dev/null and b/tts/piper/espeak-ng-data/mto_dict differ diff --git a/tts/piper/espeak-ng-data/my_dict b/tts/piper/espeak-ng-data/my_dict new file mode 100644 index 0000000..1d4730a Binary files /dev/null and b/tts/piper/espeak-ng-data/my_dict differ diff --git a/tts/piper/espeak-ng-data/nci_dict b/tts/piper/espeak-ng-data/nci_dict new file mode 100644 index 0000000..4f001af Binary files /dev/null and b/tts/piper/espeak-ng-data/nci_dict differ diff --git a/tts/piper/espeak-ng-data/ne_dict b/tts/piper/espeak-ng-data/ne_dict new file mode 100644 index 0000000..fa8e29c Binary files /dev/null and b/tts/piper/espeak-ng-data/ne_dict differ diff --git a/tts/piper/espeak-ng-data/nl_dict b/tts/piper/espeak-ng-data/nl_dict new file mode 100644 index 0000000..4b371b1 Binary files /dev/null and b/tts/piper/espeak-ng-data/nl_dict differ diff --git a/tts/piper/espeak-ng-data/no_dict b/tts/piper/espeak-ng-data/no_dict new file mode 100644 index 0000000..e1805fa Binary files /dev/null and b/tts/piper/espeak-ng-data/no_dict differ diff --git a/tts/piper/espeak-ng-data/nog_dict b/tts/piper/espeak-ng-data/nog_dict new file mode 100644 index 0000000..0819a51 Binary files /dev/null and b/tts/piper/espeak-ng-data/nog_dict differ diff --git a/tts/piper/espeak-ng-data/om_dict b/tts/piper/espeak-ng-data/om_dict new file mode 100644 index 0000000..81fa80d Binary files /dev/null and b/tts/piper/espeak-ng-data/om_dict differ diff --git a/tts/piper/espeak-ng-data/or_dict b/tts/piper/espeak-ng-data/or_dict new file mode 100644 index 0000000..ad2d191 Binary files /dev/null and b/tts/piper/espeak-ng-data/or_dict differ diff --git a/tts/piper/espeak-ng-data/pa_dict b/tts/piper/espeak-ng-data/pa_dict new file mode 100644 index 0000000..08485ee Binary files /dev/null and b/tts/piper/espeak-ng-data/pa_dict differ diff --git a/tts/piper/espeak-ng-data/pap_dict b/tts/piper/espeak-ng-data/pap_dict new file mode 100644 index 0000000..3fccee6 Binary files /dev/null and b/tts/piper/espeak-ng-data/pap_dict differ diff --git a/tts/piper/espeak-ng-data/phondata b/tts/piper/espeak-ng-data/phondata new file mode 100644 index 0000000..5b3c94a Binary files /dev/null and b/tts/piper/espeak-ng-data/phondata differ diff --git a/tts/piper/espeak-ng-data/phondata-manifest b/tts/piper/espeak-ng-data/phondata-manifest new file mode 100644 index 0000000..e7eac9a --- /dev/null +++ b/tts/piper/espeak-ng-data/phondata-manifest @@ -0,0 +1,987 @@ +# This file lists the type of data that has been compiled into the +# phondata file +# +# The first character of a line indicates the type of data: +# S - A SPECT_SEQ structure +# W - A wavefile segment +# E - An envelope +# +# Address is the displacement within phondata of this item +# +# Address Data file +# ------- --------- +W 0x00008 ustop/null +S 0x00158 vowel/@ +S 0x0021c vowel/@- +S 0x002e0 vowel/a +S 0x003e4 vowel/e +S 0x004e8 vowel/i +S 0x005ec vowel/oo +S 0x006f0 vowel/u +S 0x007f4 klatt/m-syl +S 0x008f8 m/m-syl +S 0x009bc klatt/n-syl +S 0x00ac0 n/n-syl +S 0x00b84 nn/nn-syl +W 0x00cc8 ustop/percus10 +S 0x00ed4 vowelr/r-voc +S 0x01018 vwl_hi/l-voc +S 0x0111c r/r@ +S 0x011e0 r/ra +S 0x012a4 r/re +S 0x01368 r/ri +S 0x0142c r/ro +S 0x014f0 r/ru +S 0x015b4 r/xr +S 0x01638 r/_r +S 0x016fc r/tr +S 0x01780 r/r +S 0x01844 r3/r_n +W 0x018c8 r3/rx +S 0x02c20 r/trr +S 0x02ce4 r/rr +S 0x02da8 r3/r_ +S 0x02e2c r3/r_trill2 +W 0x02ef0 r3/r_trill2.wav +S 0x035fc r3/r_trill +W 0x03700 r3/r_trill.wav +W 0x0416c r3/r_trill3.wav +S 0x045b0 r3/r_uvl +W 0x046b4 r3/r_uvl.wav +S 0x055bc l/l@ +S 0x05680 l/la +S 0x05704 l/le +S 0x05788 l/li +S 0x0584c l/lo +S 0x05950 l/lu +S 0x05a14 l/L1_@L +S 0x05b18 l/L1_aL +S 0x05c1c l/L1_eL +S 0x05ce0 l/L1_iL +S 0x05de4 l/L1_oL +S 0x05f28 l/L1_uL +S 0x0602c l/l_ +S 0x060b0 l/xl +S 0x06134 l/_l +S 0x061f8 l/tl +S 0x0627c l/l_long +S 0x06300 l/l +S 0x06384 l/L2_eL +S 0x06448 l/L2_uL +S 0x0654c l/L2_@L +S 0x06650 l/L2_aL +S 0x06754 l/L2_iL +S 0x06858 l/L2_oL +S 0x0699c l/l_@ +S 0x06a20 l/l_a +S 0x06aa4 l/l_e +S 0x06b28 l/l_i +S 0x06bec l/l_o +S 0x06cb0 l/l_u +S 0x06d34 l^/j2@ +S 0x06df8 l^/j2a +S 0x06ebc l^/j2e +S 0x06f80 l^/j2i +S 0x07044 l^/j2o +S 0x07148 l^/j2u +S 0x0724c l^/_l^ +S 0x07350 l^/l^ +S 0x07454 l^/l_rfx +S 0x07518 ll/xll +S 0x075dc ll/_ll +S 0x076e0 ll/ll +S 0x077a4 w/w@ +S 0x07868 w/wa +S 0x0792c w/we +S 0x079f0 w/wi +S 0x07ab4 w/wo +S 0x07b78 w/wu +S 0x07c3c w/xw +S 0x07cc0 w/w +S 0x07d44 w/_w +S 0x07e08 w/iw_ +S 0x07f0c w/w_ +S 0x07fd0 j/j@ +S 0x080d4 j/ja +S 0x081d8 j/je +S 0x082dc j/ji +S 0x083a0 j/jo +S 0x084e4 j/ju +S 0x085a8 j/xj +S 0x0862c j/_j +S 0x086b0 j/j_ +S 0x08774 j2/j2@ +S 0x08838 j2/j2a +S 0x088fc j2/j2e +S 0x089c0 j2/j2i +S 0x08a84 j2/j2o +S 0x08b88 j2/j2u +S 0x08c4c j2/xj2 +S 0x08cd0 j2/_j2 +S 0x08d54 klatt/m_ +S 0x08e58 klatt/m +S 0x08f5c m/m@ +S 0x09060 m/ma +S 0x09164 m/me +S 0x09268 m/mi +S 0x093ac m/mo +S 0x094b0 m/mu +S 0x095b4 m/mj +S 0x09678 m/_m +S 0x096fc m/m_ +S 0x097c0 klatt/n +S 0x098c4 n/n@ +S 0x099c8 n/na +S 0x09acc n/ne +S 0x09bd0 n/ni +S 0x09cd4 n/no +S 0x09dd8 n/nu +S 0x09edc n/nj +S 0x09fa0 n/_n +S 0x0a024 n/n_ +S 0x0a0e8 klatt/nr +S 0x0a1ec n/nr@ +S 0x0a2f0 n/nra +S 0x0a3f4 n/nre +S 0x0a4f8 n/nri +S 0x0a5fc n/nro +S 0x0a700 n/nru +S 0x0a804 n/_nr +S 0x0a888 n/nr_ +S 0x0a94c klatt/n^@ +S 0x0a9d0 klatt/n^ +S 0x0aad4 n^/n^@ +S 0x0ac58 n^/n^a +S 0x0ad9c n^/n^e +S 0x0af20 n^/n^i +S 0x0b0a4 n^/n^o +S 0x0b268 n^/n^u +S 0x0b3ec n^/_n^ +S 0x0b470 n^/n^_ +S 0x0b5b4 klatt/nn_ +S 0x0b6b8 klatt/nn +S 0x0b7bc nn/nn@ +S 0x0b880 nn/nna +S 0x0b944 nn/nne +S 0x0ba08 nn/nni +S 0x0bacc nn/nno +S 0x0bb90 nn/nnu +S 0x0bc54 nn/inn +S 0x0bd18 nn/nnj +S 0x0bd9c nn/_nn +S 0x0be20 nn/nn_ +S 0x0bee4 r3/@tap2 +S 0x0c028 r3/@tap +S 0x0c16c d/tap1 +S 0x0c230 d/tap3 +S 0x0c334 d/dr +S 0x0c3b8 r3/@tap_rfx_ +S 0x0c4fc r3/@tap_rfx +S 0x0c640 b/xb +S 0x0c704 klatt/b +W 0x0c7c8 x/b_ +W 0x0cbe0 x/b +S 0x0cd44 b/b_ +S 0x0ce08 b/b@2 +S 0x0cf0c b/b@ +S 0x0d010 b/ba +S 0x0d114 b/be +S 0x0d218 b/bi +S 0x0d31c b/bo +S 0x0d420 b/bu +S 0x0d524 b/b +S 0x0d628 d/xd +S 0x0d6ec d/d_ +W 0x0d7b0 x/d_ +S 0x0dbd8 d/d +W 0x0dc9c x/d +W 0x0deb8 x/d_dnt +S 0x0e19c dzh/xdzh +S 0x0e260 dzh/dzh_ +W 0x0e324 x/dzh_ +S 0x0ed5c dzh/dzh +W 0x0ee20 x/dzh +W 0x0f22c x/dz_pzd +S 0x0f68c dzh/xdz_pzd +S 0x0f750 klatt/dz_pzd_ +S 0x0f814 klatt/dz_pzd +S 0x0f8d8 dzh/dz_pzd_ +S 0x0f99c dzh/dz_pzd +S 0x0fa60 g/xg +S 0x0fb24 g/g_ +W 0x0fbe8 x/g_ +S 0x0ffa8 g/g +W 0x1006c x/g2 +S 0x102f4 klatt/v_ +W 0x103b8 vocw/v +S 0x10cb8 klatt/bh +S 0x10d7c voc/v_ +S 0x10e40 voc/bh +S 0x10f04 klatt/v +S 0x10fc8 voc/v +S 0x110cc voc/v#_ +S 0x11190 voc/v# +S 0x11294 voc/dh_ +W 0x11358 vocw/dh +S 0x11c60 voc/dh +S 0x11d24 voc/z_ +W 0x11de8 ufric/s_ +S 0x1289c voc/z +S 0x12960 klatt/zh_ +W 0x12a24 vocw/zh +S 0x1338c klatt/zh +S 0x13450 voc/zh_ +S 0x13514 voc/zh +W 0x135d8 vocw/zh_rfx +S 0x13e68 voc/z_pzd_ +W 0x13f2c ufric/s_pzd_ +S 0x14a30 voc/z_pzd +W 0x14af4 ufric/s_pzd +W 0x1542c ufric/sh_pzd_ +W 0x15ef4 ufric/sh_pzd +S 0x16828 voc/j +W 0x1692c ufric/ch +S 0x17044 klatt/qqh_ +W 0x17108 vocw/Q_ +S 0x17914 klatt/qqh +W 0x179d8 vocw/Q +S 0x181e4 voc/Q_ +S 0x182a8 voc/Q +S 0x1836c voc/Q_ulv +W 0x18470 ufric/xx +W 0x18f6c ustop/p_ +W 0x1977c ustop/pr +W 0x19bc4 ustop/p_unasp +W 0x19ef0 ustop/pl +W 0x1a2e8 ustop/p +W 0x1a658 ustop/t_ +W 0x1ab18 ustop/tr +W 0x1b278 ustop/t_dnt +W 0x1b688 ustop/t +W 0x1babc ustop/t_hi +W 0x1bd88 ustop/tsh_ +W 0x1c6c8 ustop/tsh +W 0x1cd08 ustop/ts_pzd +W 0x1d40c ustop/t_pzd +W 0x1d858 ustop/c +W 0x1db08 ustop/k_ +W 0x1dfc4 ustop/kr +W 0x1e604 ustop/ki +W 0x1ebcc ustop/kl +W 0x1f1fc ustop/k_unasp +W 0x1f6d0 ustop/k +W 0x1fba4 ustop/q +W 0x1fd10 ustop/q_u +W 0x1fe30 ufric/f_ +W 0x20900 ufric/f +W 0x210f0 ufric/th_ +W 0x21970 ufric/th +W 0x22208 ufric/s! +W 0x22ab8 ufric/s +W 0x23258 ufric/sh_ +W 0x23d08 ufric/sh +W 0x246b8 ufric/sh_rfx +W 0x25054 ufric/l# +W 0x25a98 ufric/ch_ +W 0x263c0 ufric/x_ +W 0x26f1c ufric/x_hr +W 0x27830 h/h@ +W 0x27d84 h/ha +W 0x28400 h/he +W 0x28a94 h/hi +W 0x2902c h/ho +W 0x296e8 h/hu +W 0x29e0c h/h_ +W 0x2a4b8 ustop/ts_ +W 0x2ae18 ustop/ts +S 0x2b874 d/xdz +W 0x2b938 ustop/p_unasp_ +W 0x2ba74 ustop/p_asp +W 0x2bf78 ustop/t_short +W 0x2c1bc ustop/ts_pzd_ +W 0x2c680 ustop/ts_pzd2 +W 0x2c9b4 ustop/k_asp +W 0x2d0b8 ustop/k_asp_e +W 0x2d820 ustop/k_asp_a +W 0x2e0a4 ufric/s_continue +S 0x2e854 vowel/a# +S 0x2e958 vowel/a_2 +S 0x2ea5c vowel/ee_1 +S 0x2eba0 vowel/o +S 0x2ece4 vowel/oo_4 +S 0x2ede8 vowel/u_bck +S 0x2eeec vowel/uu_2 +S 0x2eff0 vowel/y +S 0x2f134 vowel/y# +S 0x2f238 vdiph/au_4 +S 0x2f3bc vdiph/eu +S 0x2f500 vdiph2/iu +S 0x2f684 vdiph/ai +S 0x2f7c8 vdiph/ei +S 0x2f90c vdiph/eei +S 0x2fa90 vdiph/oi +S 0x2fc54 vdiph/ui +S 0x2fd98 w/w2 +W 0x2fe5c ustop/t_dnt2 +S 0x300b4 vowel/ii_5 +S 0x301b8 vowel/e_mid2 +S 0x302bc vowel/ii#_2 +S 0x303c0 vdiph/ai_5 +S 0x30504 vowel/a_6 +S 0x30608 vdiph/@i_2 +S 0x3074c vowel/@_3 +S 0x30810 vowel/o-_4 +S 0x30914 vowel/u_2 +W 0x30a18 ustop/tsh_unasp +W 0x30ed8 ustop/k_ejc +W 0x31688 vwl_ar/hah +W 0x325ec vwl_ar/dhad +W 0x3364c vwl_ar/shin +S 0x34428 vowel/aa_7 +S 0x3452c vowel/u_7 +S 0x34630 vowel/ii# +S 0x34734 vowel/oe +S 0x34838 vowel/o_mid +S 0x3493c vowel/@_2 +S 0x34a40 vowel/ee +S 0x34b44 vowel/aa_8 +S 0x34c48 vowel/0_3 +S 0x34d4c vowel/e_6 +S 0x34e50 vowel/ii +E 0x34f54 envelope/i_risefall +E 0x34fd4 envelope/p_fall +E 0x35054 envelope/p_214 +E 0x350d4 envelope/p_rise +E 0x35154 envelope/p_level +S 0x351d4 n/n_long_ +S 0x35298 nn/nn2_ +W 0x3535c ustop/t_unasp2 +W 0x35468 ustop/k_unasp_ +W 0x35724 ustop/tsh_pzd_unasp +W 0x35f74 ustop/tsh_pzd +W 0x36ab4 ufric/sh_pzd2 +W 0x37604 ustop/ts_unasp +W 0x37e7c ustop/ts_rfx_unasp +W 0x38a68 ustop/ts_rfx +S 0x39748 vwl_zh/a_n +S 0x3984c vowel/aa_2 +S 0x399d0 vowel/a_3 +S 0x39ad4 vdiph/ai_6 +S 0x39c98 vwl_zh/aau +S 0x39ddc vowel/@_bck +S 0x39ee0 vowel/3_2 +S 0x3a024 vowelr/V3_r +S 0x3a168 vowel/ee_2 +S 0x3a26c vdiph2/ei_4 +S 0x3a3b0 vowel/ii_2 +S 0x3a4b4 vowel/i#_7 +S 0x3a5b8 vowel/i#_6 +S 0x3a6bc vwl_zh/iaa +S 0x3a800 vwl_zh/iaau +S 0x3a984 vwl_zh/ie +S 0x3aac8 vdiph2/iioo +S 0x3ac0c vwl_zh/iou +S 0x3ad90 vnasal/oo_n2 +S 0x3aed4 vdiph2/o_oo +S 0x3b018 vowel/8 +S 0x3b11c vdiph/8u +S 0x3b260 vwl_zh/ong +S 0x3b3e4 vnasal/u_n +S 0x3b4e8 vwl_zh/uaa +S 0x3b62c vdiph2/oa +S 0x3b770 vwl_zh/uai +S 0x3b8f4 vdiph2/o@ +S 0x3ba78 vwl_zh/uei +S 0x3bbfc vwl_zh/uo +S 0x3bd80 vowel/uu_3 +S 0x3be44 vowel/y_2 +S 0x3bf88 vdiph2/yu +S 0x3c10c vwl_zh/y& +S 0x3c250 vwl_zh/yee +S 0x3c394 vdiph2/y@ +S 0x3c4d8 vdiph/yi +S 0x3c61c vowel/ii_3 +S 0x3c720 vowel/oo_2 +S 0x3c824 vowel/i# +S 0x3c928 vowel/o_2 +S 0x3ca6c vdiph/aai_2 +S 0x3cc30 vdiph/ai_2 +S 0x3cdf4 vdiph2/iu_4 +S 0x3cf38 vdiph/ooi +S 0x3d0fc vdiph/ui_3 +S 0x3d280 vowel/a#_3 +S 0x3d384 r/a_ +S 0x3d448 vowel/i_4 +S 0x3d54c vowel/& +S 0x3d650 vowel/a_8 +S 0x3d754 vowel/o_5 +S 0x3d858 vowel/V_4 +S 0x3d95c vowel/yy +S 0x3da60 vowel/V +S 0x3db64 r/aa +S 0x3dc68 r2/r2@ +S 0x3dd2c r2/r2a +S 0x3ddf0 r2/r2e +S 0x3def4 r2/r2i +S 0x3dfb8 r2/r2o +S 0x3e07c r2/r2u +S 0x3e140 vowel/@_6 +S 0x3e244 vwl_en/@L +S 0x3e308 vowel/ee_5 +S 0x3e40c vowel/ii#_3 +S 0x3e510 vowel/ii_4 +S 0x3e614 vowel/ii_7 +S 0x3e718 vowel/0 +S 0x3e81c vowel/V_2 +S 0x3e920 vowel/8_2 +S 0x3ea24 vowel/uu +S 0x3eb28 vowel/3_en +S 0x3ec6c w/wi2 +S 0x3ed70 vowel/i_en +S 0x3eeb4 vowel/oo_en +S 0x3eff8 vwl_en/u_L +S 0x3f13c vdiph2/uw_2 +S 0x3f280 vdiph/au +S 0x3f404 vdiph/@u_en +S 0x3f588 vdiph2/ii@ +S 0x3f70c vdiph2/8@ +S 0x3f850 vdiph2/uu@ +S 0x3f9d4 vwl_en/aI@ +S 0x3fb98 vwl_en/aU@ +S 0x3fd1c vowelr/V_r +S 0x3fe60 vnasal/aa_n2 +S 0x3ffa4 vowel/oo_1 +S 0x400e8 vdiph/eei_2 +S 0x4022c vdiph/ooi_2 +S 0x403f0 vowel/aa_9 +S 0x404f4 vowel/aa +S 0x405f8 vowel/e# +S 0x406fc vowel/e_7 +S 0x40800 vowel/ee#_2 +S 0x40904 vowel/i_8 +S 0x40a08 vowel/i_7 +S 0x40b0c vowel/u_bck2 +S 0x40c10 vowel/u_bck3 +S 0x40d14 vowel/u_5 +S 0x40e18 vowel/8_7 +S 0x40f1c vowel/8_4 +S 0x41020 vdiph/&i +S 0x41164 vdiph/@i +W 0x412a8 ufric/s_pal +S 0x41b5c d/xd_pzd +W 0x41c20 x/d_pzd +S 0x42034 vdiph/ou_2 +S 0x42178 vowel/aa# +S 0x4227c vowel/uu_4 +S 0x42380 vdiph/aai_3 +S 0x42504 vdiph/y#i +S 0x42648 vdiph/ui_4 +S 0x4278c vdiph/aau +S 0x42910 vdiph/ou +S 0x42a54 vdiph/eu_2 +S 0x42b98 vdiph2/iu_2 +S 0x42d1c vdiph/&y +S 0x42e60 vdiph/eey +S 0x42fa4 vdiph/y#y +S 0x430e8 vdiph2/iy +S 0x4322c vdiph2/uo +S 0x43370 vdiph2/ie +S 0x434b4 vdiph2/y-y# +S 0x435f8 r3/r_trill_short +W 0x436fc h/hu_fi +S 0x43fec vowel/@_4 +S 0x440b0 vowel/ee_4 +S 0x441b4 vowel/u#_2 +S 0x442b8 vowel/oe_2 +S 0x443bc vwl_fr/y2r +S 0x44440 vwl_fr/e_2r +S 0x444c4 vwl_fr/aa2r +S 0x44548 vwl_fr/ee2r +S 0x4460c vwl_fr/oo2r +S 0x446d0 vwl_fr/@2r +S 0x44754 vwl_fr/a2r +S 0x447d8 vwl_fr/e2r +S 0x4485c vwl_fr/i2r +S 0x448e0 vwl_fr/o2r +S 0x44964 vwl_fr/u2r +S 0x449e8 vwl_fr/re2 +S 0x44a6c vwl_fr/r@2 +S 0x44af0 vwl_fr/raa +S 0x44b74 vwl_fr/ree +S 0x44bf8 vwl_fr/ry +S 0x44c7c vwl_fr/rw +S 0x44d00 vwl_fr/roo +S 0x44d84 vwl_fr/rj +S 0x44e08 vwl_fr/r@ +S 0x44e8c vwl_fr/ra +S 0x44f10 vwl_fr/re +S 0x44f94 vwl_fr/ri +S 0x45018 vwl_fr/ro +S 0x4509c vwl_fr/ru +S 0x45120 vwl_fr/r +S 0x451a4 vwl_fr/trr +S 0x45268 vwl_fr/rr +S 0x452ec vwl_fr/r_@ +S 0x45370 vwl_fr/r_a +S 0x453f4 vwl_fr/r_e +S 0x45478 vwl_fr/r_i +S 0x454fc vwl_fr/r_o +S 0x45580 vwl_fr/r_u +S 0x45604 vwl_fr/r_y +S 0x45688 vwl_fr/r_n +S 0x4574c vwl_fr/r_ +S 0x457d0 vwl_fr/tr +S 0x458d4 vwl_fr/br +S 0x459d8 vwl_fr/lo +S 0x45a5c l/l_y +S 0x45ae0 vowel/@_hgh +S 0x45ba4 vowel/a_7 +S 0x45ca8 vowel/e_8 +S 0x45dac vowel/e_mid +S 0x45eb0 vwl_fr/j +S 0x45fb4 vowel/o_8 +S 0x460f8 vowel/o_mid2 +S 0x461fc vwl_fr/wa +S 0x462c0 vnasal/aa_n4 +S 0x46404 vnasal/W_n +S 0x46548 vnasal/o_n5 +S 0x4668c b/xbr +S 0x46750 b/br +S 0x467d4 d/xdr +S 0x46898 g/xgr +S 0x4695c g/gr +W 0x46a20 x/g +W 0x46ce0 ustop/t_short_ +S 0x46f6c vwl_af/r@ +S 0x47030 vwl_af/@ +S 0x470f4 vowel/a_4 +S 0x471f8 vdiph/@u_3 +S 0x4737c vdiph2/i@ +S 0x47540 vdiph2/u@ +S 0x476c4 vnasal/a#_n2 +S 0x477c8 vnasal/a_n +S 0x4790c vnasal/e_n +S 0x47a10 vnasal/i_n +S 0x47b14 vnasal/o_n +S 0x47c58 vowel/yy_4 +S 0x47d5c vowel/o- +S 0x47e60 vwl_lv/a +S 0x47f64 vwl_lv/aa +S 0x48028 vwl_lv/e +S 0x480ec vwl_lv/ee +S 0x481b0 vwl_lv/i +S 0x48274 vwl_lv/ii +S 0x48338 vwl_lv/o +S 0x483fc vwl_lv/oo +S 0x484c0 vwl_lv/u +S 0x48584 vwl_lv/uu +S 0x48648 vdiph/aai +S 0x487cc vdiph2/ie_2 +S 0x48910 vdiph2/ua +S 0x48a14 vowel/@_low +S 0x48ad8 vowel/V_3 +S 0x48bdc vowel/i_fnt +S 0x48ce0 vowel/ii_6 +S 0x48da4 vowel/e_2 +S 0x48ea8 vdiph/ee-e +S 0x48fec vowel/a_5 +S 0x490f0 vowel/uu_bck +S 0x491f4 vnasal/i_n2 +S 0x492f8 vnasal/ii_n +S 0x493fc vnasal/ee_n2 +S 0x49540 vnasal/V_n +S 0x49644 vdiph/aau_3 +S 0x497c8 d/xd3 +W 0x4988c ustop/th_rfx2 +S 0x49e90 g2/xg +W 0x49f54 ustop/percus02 +W 0x4a284 ustop/ts2 +S 0x4a754 vowel/y#_2 +S 0x4a858 vowel/e_3 +S 0x4a95c vowel/e_e +S 0x4aa60 vowel/a#_2 +S 0x4ab64 vowel/oo_5 +S 0x4ac68 vowel/y## +S 0x4ad6c vowel/y#_3 +S 0x4ae70 vdiph/ai_7 +S 0x4aff4 vdiph/ou_3 +S 0x4b0f8 vdiph/y#i_2 +S 0x4b23c m/m#_ +S 0x4b340 n/n#_ +S 0x4b444 n^/n^#_ +S 0x4b548 nn/nn#_ +W 0x4b64c ufric/tl# +S 0x4bfb4 r3/r#_ +S 0x4c038 vowel/oo_3 +W 0x4c0fc ustop/k_asp_u +W 0x4c92c ufric/x2 +S 0x4d284 vowel/aa_6 +S 0x4d3c8 vowel/u#_7 +S 0x4d4cc vowel/V_6 +W 0x4d5d0 ustop/t_unasp +W 0x4d708 ustop/ts_pzd3 +S 0x4dd88 vowel/i_2 +S 0x4de8c vdiph/aau_2 +S 0x4dfd0 vdiph/ae +S 0x4e114 vdiph/eeu_2 +S 0x4e258 vdiph/ae_2 +S 0x4e3dc vdiph/eei_5 +S 0x4e520 vwl_ru/ee +S 0x4e664 vdiph2/ea +S 0x4e7a8 vowel/i_3 +S 0x4e8ac vowel/i_6 +S 0x4e9b0 vdiph2/uaa +S 0x4eb34 vdiph/eei_3 +S 0x4ec78 vwl_lv/e2 +S 0x4ed3c vwl_lv/ee2 +S 0x4ee00 vwl_lv/y +S 0x4ef04 vwl_lv/yy +S 0x4f008 vwl_lv/ai +S 0x4f18c vwl_lv/au +S 0x4f310 vwl_lv/ei +S 0x4f454 vwl_lv/ie +S 0x4f598 vwl_lv/iu +S 0x4f71c vwl_lv/ui +S 0x4f860 vwl_lv/ua +S 0x4f964 vwl_lv/oi +W 0x4fb28 h/h-lv +S 0x504a8 l^/l^_ +W 0x505ac myanmar/k +W 0x5149c myanmar/kh +W 0x520b4 myanmar/g.wav +S 0x52ef8 myanmar/ny +W 0x530bc myanmar/s +W 0x54258 myanmar/hs +W 0x553f4 myanmar/z +W 0x564dc myanmar/t_short +W 0x5662c myanmar/ht +W 0x571b4 myanmar/d +W 0x57d28 myanmar/p +W 0x58c54 myanmar/t_hi +W 0x58f20 myanmar/h +S 0x5a228 myanmar/yy +S 0x5a42c myanmar/a +S 0x5a4b0 myanmar/ky +W 0x5a6f4 myanmar/by.wav +W 0x5ac00 myanmar/ch.wav +W 0x5b454 myanmar/gya.wav +W 0x5c7d8 myanmar/htya.wav +W 0x5db1c myanmar/phya.wav +W 0x5e7d4 myanmar/pya.wav +W 0x5fc58 myanmar/ty.wav +W 0x60f18 myanmar/sh.wav +S 0x61b2c myanmar/a01 +S 0x61c30 myanmar/a02 +S 0x61db4 myanmar/a03 +S 0x62038 myanmar/a04 +S 0x620fc myanmar/a05 +S 0x62200 myanmar/a06 +S 0x62584 myanmar/a07 +S 0x626c8 myanmar/a08 +S 0x6284c myanmar/a09 +S 0x62b50 myanmar/a11 +S 0x62cd4 myanmar/a12 +S 0x62f98 myanmar/a14 +S 0x6315c myanmar/a13 +S 0x633e0 myanmar/a17 +S 0x636a4 myanmar/a16 +S 0x639a8 myanmar/a20 +S 0x63b2c myanmar/a19 +S 0x63d70 myanmar/a21 +S 0x64034 myanmar/a23 +S 0x64138 myanmar/a22 +S 0x642bc myanmar/a24 +S 0x64600 myanmar/a29 +S 0x64744 myanmar/a28 +S 0x64988 myanmar/a30 +S 0x64d0c myanmar/a32 +S 0x64f50 myanmar/a31 +S 0x65214 myanmar/a33 +S 0x65318 myanmar/a35 +S 0x655dc myanmar/a34 +S 0x658a0 myanmar/a36 +S 0x65ba4 myanmar/a43 +S 0x65de8 myanmar/a42 +S 0x6602c myanmar/a44 +S 0x66430 myanmar/a49 +S 0x665b4 myanmar/a50 +S 0x66838 myanmar/a46 +S 0x66a3c myanmar/a45 +S 0x66c40 myanmar/a47 +S 0x66f44 myanmar/a25 +S 0x67088 myanmar/a26 +S 0x672cc myanmar/a27 +S 0x67490 myanmar/a37 +S 0x67654 myanmar/a38 +S 0x67798 myanmar/a39 +S 0x679dc myanmar/a40 +S 0x67b60 myanmar/a41 +S 0x67ce4 vowel/y_4 +S 0x67de8 vwl_no/y# +S 0x67eec vwl_no/& +S 0x67ff0 vwl_no/u# +S 0x680f4 vwl_no/u#2 +S 0x68238 vdiph/0i_2 +S 0x683bc vdiph/ai_3 +S 0x68500 vwl_no/y#y +S 0x68644 vwl_no/au- +S 0x68808 vowel/ee# +S 0x6890c vnasal/ee_u_n +S 0x68a90 vnasal/oo_n3 +S 0x68c14 vowel/aa_3 +W 0x68d18 vocw/Q2 +W 0x695e4 ustop/tsh_asp +W 0x6a24c x/g3 +S 0x6a434 vwl_ro/mi +S 0x6a578 vwl_ru/ii- +S 0x6a63c vwl_ru/ii +S 0x6a740 vwl_ru/ii# +S 0x6a804 vwl_ru/i# +S 0x6a908 vwl_ru/e +S 0x6aa0c vwl_ru/E# +S 0x6ab10 vwl_ru/E@ +S 0x6ac14 vwl_ru/o +S 0x6ad18 vwl_ru/oo +S 0x6addc vwl_ru/u +S 0x6aee0 vwl_ru/u# +S 0x6b024 vwl_ru/u#u +S 0x6b168 vwl_ru/8 +S 0x6b22c vwl_ru/ju +S 0x6b330 vwl_ru/ja +S 0x6b4b4 vwl_ru/a +S 0x6b5b8 vwl_ru/aa +S 0x6b6bc r3/r_ru2 +W 0x6b7c0 r3/r_ru +S 0x6bac4 vwl_it/o_open +S 0x6bbc8 vdiph/eeu +S 0x6bd0c vdiph/au_2 +S 0x6be90 vdiph/ooi_3 +S 0x6c014 vdiph/aau_4 +S 0x6c198 vowel/8_6 +S 0x6c29c vdiph/ooi_4 +S 0x6c3e0 vdiph2/ye +S 0x6c524 l/l_front_ +S 0x6c668 l/l_front +S 0x6c76c l/l_4 +S 0x6c830 vowel/ee_6 +S 0x6c8f4 vowel/y_5 +S 0x6ca38 vowel/yy_3 +S 0x6cb3c vowel/oe_4 +S 0x6cc00 vowel/aa_4 +W 0x6cd04 ufric/sx_sv +S 0x6d54c vowel/o_4 +W 0x6d650 ustop/t_hard +W 0x6d890 ufric/sh3 +S 0x6e224 vwl_tn/r@ +S 0x6e2e8 vwl_tn/@ +S 0x6e3ac vwl_tn/I +S 0x6e470 vdiph/i@_2 +S 0x6e5f4 vowel/0_2 +W 0x6e6f8 ufric/tlh +E 0x6fc14 envelope/p_fallrise +E 0x6fc94 envelope/vi_5amp +E 0x6fd14 envelope/p_512 +E 0x6fd94 envelope/vi_6amp +S 0x6fe14 vietnam/a_2 +S 0x6ff58 vietnam/aa +S 0x7005c vietnam/e_e_2 +S 0x70160 vietnam/e +S 0x70264 vietnam/e_short_1 +S 0x703e8 vietnam/i +S 0x704ec vietnam/i_2 +S 0x705f0 vietnam/oo +S 0x70734 vietnam/o_2 +S 0x70878 vietnam/u +S 0x7097c vietnam/y_2 +S 0x70a80 vietnam/V_2 +S 0x70b84 vietnam/@_2 +S 0x70c88 vdiph/&i_2 +S 0x70dcc vdiph/u-i +S 0x70f10 vdiph/@u +S 0x71054 vdiph2/ii@_3 +S 0x711d8 vietnam/y@ +S 0x7131c vietnam/u@ +S 0x714a0 vietnam/o# +S 0x715e4 vietnam/O_short_2 +S 0x71728 vietnam/oe +S 0x7186c vietnam/ie +S 0x719b0 vnasal/oi_n +S 0x71b74 vdiph/@i_3 +S 0x71cb8 vowel/u_3 +W 0x71dbc vietnam/c_2 +W 0x722f8 vietnam/c +S 0x72834 n/nm +S 0x728f8 l/l_vi +W 0x729fc vietnam/th +W 0x72fec vietnam/tr +W 0x732dc vietnam/dda_2 +W 0x737a8 vietnam/ch +W 0x73d04 vietnam/w +S 0x73f5c vietnam/_w +S 0x74020 vietnam/w_ +S 0x740e4 vietnam/n^_ +W 0x74228 vietnam/hi +W 0x74594 vietnam/hu +S 0x74c18 vwl_zh/ang +S 0x74e1c vwl_zh/aang +S 0x75060 vwl_zh/eng +S 0x75264 vwl_zh/ing +S 0x754a8 vwl_zh/ng +S 0x755ec vwl_zh/oeng +S 0x757b0 vwl_zh/ung +S 0x75934 vowel/8_3 +S 0x75a38 vdiph/y#y_2 +W 0x75bbc ustop/tsh_sr +E 0x764d4 envelope/chr_fall21 +E 0x76554 envelope/chr_level2 +E 0x765d4 envelope/chr_rise23 +E 0x76654 envelope/chr_fall32 +E 0x766d4 envelope/chr_level3 +E 0x76754 envelope/chr_rise4 +E 0x767d4 envelope/chr_fall43 +S 0x76854 vnasal/a#_n +W 0x76958 x/dz_pzd_ +S 0x77048 vwl_es/oo_ +S 0x7710c vwl_es/ooi_ +S 0x77290 vwl_es/ooi +S 0x77414 voc/v2 +W 0x774d8 ufric/z_eu +W 0x77ee0 ufric/ts_eu +W 0x78888 ufric/tz_eu +S 0x792f4 vdiph/0i +S 0x794b8 vdiph/oou +S 0x795fc vwl_it/a +S 0x79700 vwl_it/e_open +S 0x79804 vwl_it/i +S 0x79908 vwl_it/o +S 0x79a4c vwl_it/u +S 0x79b50 vowel/8_5 +S 0x79c54 vowel/o_7 +S 0x79d58 vdiph/eeu_3 +S 0x79e9c vnasal/a#u_n +S 0x79fe0 vowel/ee_3 +S 0x7a0a4 d/x_tap +S 0x7a168 d/tap2 +S 0x7a22c vwl_ro/li +S 0x7a330 vwl_ro/ni +S 0x7a434 vwl_ro/ii- +S 0x7a4f8 vowel/i#_5 +S 0x7a5fc vdiph/ii +S 0x7a7c0 vdiph/i#i +S 0x7a904 vdiph2/uw_3 +S 0x7aa48 vdiph2/eo +S 0x7abcc vdiph2/e_u +S 0x7ad10 d/tap_i +S 0x7add4 d/tap +S 0x7ae98 vowel/u# +S 0x7af9c vowel/@_fr +S 0x7b060 vowel/o-_2 +S 0x7b164 vowel/aa_5 +S 0x7b2a8 vwl_en_n/O@ +S 0x7b3ac vdiph2/uw_4 +S 0x7b4f0 vdiph2/ee@ +S 0x7b634 vowel/ii_final +S 0x7b738 vowel/o-_3 +S 0x7b83c vwl_en_rp/aa +S 0x7b980 vowel/3_3 +S 0x7bac4 vowel/uu#_2 +S 0x7bbc8 vdiph/au_3 +S 0x7bd4c vdiph/@u_2 +S 0x7be90 vdiph2/ei_2 +S 0x7bfd4 vdiph2/ee@_2 +S 0x7c118 vwl_en_rp/i@ +S 0x7c29c vwl_en_rp/aU@ +S 0x7c420 vowel/e_5 +S 0x7c524 vowel/u#_4 +S 0x7c628 vowelr/aa_r +S 0x7c7ec vowelr/e_r +S 0x7c970 vowel/i_5 +S 0x7ca74 vwl_en_us/or +S 0x7cbb8 vowelr/oo_r +S 0x7ccbc vdiph/au# +S 0x7ce00 vowel/o_3 +S 0x7cf44 vwl_en/aI@_2 +S 0x7d0c8 vdiph2/e@ +S 0x7d20c vowelr/i_r +S 0x7d350 vdiph2/u#@ +S 0x7d494 vwl_en/@L_2 +S 0x7d598 vwl_en_us/3_us +S 0x7d69c vowel/@_low2 +S 0x7d760 vwl_en_us/ar +S 0x7d8a4 vwl_en_us/a +S 0x7d9a8 vwl_en_us/ee +S 0x7daac vwl_en_us/aar +S 0x7dc30 vwl_en_us/3_us2 +S 0x7dd74 vwl_en_us/oor +S 0x7def8 vdiph2/uw_6 +S 0x7e03c vdiph/aoo +S 0x7e180 vwl_en_us/ai +S 0x7e304 vwl_en_us/er +S 0x7e488 vwl_en_us/ir +S 0x7e60c vwl_en_us/ur +S 0x7e750 vwl_en_us/ai@ +S 0x7e8d4 vwl_en_us/ai3 +S 0x7ead8 vwl_en_us/aU@ +S 0x7ec9c klatt/x_tap +S 0x7ed60 klatt/tap2 +S 0x7ee24 vwl_en_us_nyc/a_raised +S 0x7ef28 vwl_en_us_nyc/a +S 0x7f02c vwl_en_us_nyc/0_3 +S 0x7f130 vwl_en_us_nyc/aa_8 +S 0x7f234 vwl_en_us_nyc/@i +S 0x7f3f8 vowel/@_fnt +S 0x7f4fc vdiph2/ei_3 +S 0x7f640 vdiph/Vu_2 +S 0x7f7c4 vdiph2/i@_2 +S 0x7f948 vwl_en/ooi@ +S 0x7fb0c vwl_af/I +S 0x7fbd0 l/L_eL_af +S 0x7fc94 vowel/y_3 +S 0x7fd98 vdiph2/iu_3 +S 0x7ff5c vdiph/Vu +S 0x800e0 vdiph/ai_4 +S 0x80264 vdiph/oi_2 +S 0x80428 vdiph/ui_2 +S 0x805ac vdiph2/y#@ +S 0x806f0 vnasal/aa_n3 +S 0x80834 vnasal/o_n2 +S 0x80978 vdiph/aau_6 +S 0x80afc vwl_de/y# +S 0x80c00 l/l_3 +S 0x80c84 j/_j_short +S 0x80d08 vdiph2/i@_3 +S 0x80e8c vwl_de/uu_@ +S 0x80fd0 vdiph2/ii@_2 +S 0x81154 vowel/ii_8 +S 0x81258 vowel/y#_4 +S 0x8135c vowel/o_6 +S 0x81460 vowel/a#_4 +S 0x81564 vdiph/y#y_3 +S 0x81668 vdiph/ou_4 +S 0x817ac voc/Q_less +S 0x81870 vnasal/&_n +W 0x81974 ustop/ki_ejc +S 0x81d80 vdiph/Vi +S 0x81f04 vowel/u_6 +S 0x82008 vowel/u#_3 +S 0x8210c vdiph/ai_8 +S 0x82290 voc/murmur1 +S 0x823d4 vdiph/@i_4 +S 0x82558 vnasal/ai_n +S 0x8269c vdiph/a#u +S 0x82820 vnasal/au_n +S 0x82964 d/dr2 +S 0x82a28 vowel/&_2 +W 0x82b2c ustop/tsh_unasp2 +W 0x82f74 r3/rz_cs +S 0x83a20 voc/zh_2 +W 0x83ae4 ustop/tsh2 +S 0x841f0 dzh/dzh2 +W 0x842b4 ustop/t_sr +S 0x845ec d/d_dnt +W 0x846b0 ufric/ch_sr +W 0x85290 ufric/x_sr +W 0x85b10 ustop/ts_sr +S 0x862cc vowel/&_3 +S 0x863d0 vwl_fr/@R +S 0x864d4 vietnam/a diff --git a/tts/piper/espeak-ng-data/phonindex b/tts/piper/espeak-ng-data/phonindex new file mode 100644 index 0000000..03ba806 Binary files /dev/null and b/tts/piper/espeak-ng-data/phonindex differ diff --git a/tts/piper/espeak-ng-data/phontab b/tts/piper/espeak-ng-data/phontab new file mode 100644 index 0000000..5c25ca1 Binary files /dev/null and b/tts/piper/espeak-ng-data/phontab differ diff --git a/tts/piper/espeak-ng-data/piqd_dict b/tts/piper/espeak-ng-data/piqd_dict new file mode 100644 index 0000000..7924788 Binary files /dev/null and b/tts/piper/espeak-ng-data/piqd_dict differ diff --git a/tts/piper/espeak-ng-data/pl_dict b/tts/piper/espeak-ng-data/pl_dict new file mode 100644 index 0000000..dcb4132 Binary files /dev/null and b/tts/piper/espeak-ng-data/pl_dict differ diff --git a/tts/piper/espeak-ng-data/pt_dict b/tts/piper/espeak-ng-data/pt_dict new file mode 100644 index 0000000..c801973 Binary files /dev/null and b/tts/piper/espeak-ng-data/pt_dict differ diff --git a/tts/piper/espeak-ng-data/py_dict b/tts/piper/espeak-ng-data/py_dict new file mode 100644 index 0000000..47b8123 Binary files /dev/null and b/tts/piper/espeak-ng-data/py_dict differ diff --git a/tts/piper/espeak-ng-data/qdb_dict b/tts/piper/espeak-ng-data/qdb_dict new file mode 100644 index 0000000..18a6895 Binary files /dev/null and b/tts/piper/espeak-ng-data/qdb_dict differ diff --git a/tts/piper/espeak-ng-data/qu_dict b/tts/piper/espeak-ng-data/qu_dict new file mode 100644 index 0000000..afb6dcf Binary files /dev/null and b/tts/piper/espeak-ng-data/qu_dict differ diff --git a/tts/piper/espeak-ng-data/quc_dict b/tts/piper/espeak-ng-data/quc_dict new file mode 100644 index 0000000..2361f7b Binary files /dev/null and b/tts/piper/espeak-ng-data/quc_dict differ diff --git a/tts/piper/espeak-ng-data/qya_dict b/tts/piper/espeak-ng-data/qya_dict new file mode 100644 index 0000000..9468943 Binary files /dev/null and b/tts/piper/espeak-ng-data/qya_dict differ diff --git a/tts/piper/espeak-ng-data/ro_dict b/tts/piper/espeak-ng-data/ro_dict new file mode 100644 index 0000000..22f7dfb Binary files /dev/null and b/tts/piper/espeak-ng-data/ro_dict differ diff --git a/tts/piper/espeak-ng-data/ru_dict b/tts/piper/espeak-ng-data/ru_dict new file mode 100644 index 0000000..ac39ad9 Binary files /dev/null and b/tts/piper/espeak-ng-data/ru_dict differ diff --git a/tts/piper/espeak-ng-data/sd_dict b/tts/piper/espeak-ng-data/sd_dict new file mode 100644 index 0000000..9d14460 Binary files /dev/null and b/tts/piper/espeak-ng-data/sd_dict differ diff --git a/tts/piper/espeak-ng-data/shn_dict b/tts/piper/espeak-ng-data/shn_dict new file mode 100644 index 0000000..212227d Binary files /dev/null and b/tts/piper/espeak-ng-data/shn_dict differ diff --git a/tts/piper/espeak-ng-data/si_dict b/tts/piper/espeak-ng-data/si_dict new file mode 100644 index 0000000..9634893 Binary files /dev/null and b/tts/piper/espeak-ng-data/si_dict differ diff --git a/tts/piper/espeak-ng-data/sjn_dict b/tts/piper/espeak-ng-data/sjn_dict new file mode 100644 index 0000000..ba76f7c Binary files /dev/null and b/tts/piper/espeak-ng-data/sjn_dict differ diff --git a/tts/piper/espeak-ng-data/sk_dict b/tts/piper/espeak-ng-data/sk_dict new file mode 100644 index 0000000..cd99f8f Binary files /dev/null and b/tts/piper/espeak-ng-data/sk_dict differ diff --git a/tts/piper/espeak-ng-data/sl_dict b/tts/piper/espeak-ng-data/sl_dict new file mode 100644 index 0000000..e13eb49 Binary files /dev/null and b/tts/piper/espeak-ng-data/sl_dict differ diff --git a/tts/piper/espeak-ng-data/smj_dict b/tts/piper/espeak-ng-data/smj_dict new file mode 100644 index 0000000..203230f Binary files /dev/null and b/tts/piper/espeak-ng-data/smj_dict differ diff --git a/tts/piper/espeak-ng-data/sq_dict b/tts/piper/espeak-ng-data/sq_dict new file mode 100644 index 0000000..fccdbf3 Binary files /dev/null and b/tts/piper/espeak-ng-data/sq_dict differ diff --git a/tts/piper/espeak-ng-data/sr_dict b/tts/piper/espeak-ng-data/sr_dict new file mode 100644 index 0000000..63576dd Binary files /dev/null and b/tts/piper/espeak-ng-data/sr_dict differ diff --git a/tts/piper/espeak-ng-data/sv_dict b/tts/piper/espeak-ng-data/sv_dict new file mode 100644 index 0000000..a4e3f6d Binary files /dev/null and b/tts/piper/espeak-ng-data/sv_dict differ diff --git a/tts/piper/espeak-ng-data/sw_dict b/tts/piper/espeak-ng-data/sw_dict new file mode 100644 index 0000000..601f310 Binary files /dev/null and b/tts/piper/espeak-ng-data/sw_dict differ diff --git a/tts/piper/espeak-ng-data/ta_dict b/tts/piper/espeak-ng-data/ta_dict new file mode 100644 index 0000000..7e51266 Binary files /dev/null and b/tts/piper/espeak-ng-data/ta_dict differ diff --git a/tts/piper/espeak-ng-data/te_dict b/tts/piper/espeak-ng-data/te_dict new file mode 100644 index 0000000..36167c2 Binary files /dev/null and b/tts/piper/espeak-ng-data/te_dict differ diff --git a/tts/piper/espeak-ng-data/th_dict b/tts/piper/espeak-ng-data/th_dict new file mode 100644 index 0000000..307d63d Binary files /dev/null and b/tts/piper/espeak-ng-data/th_dict differ diff --git a/tts/piper/espeak-ng-data/tk_dict b/tts/piper/espeak-ng-data/tk_dict new file mode 100644 index 0000000..e0dace4 Binary files /dev/null and b/tts/piper/espeak-ng-data/tk_dict differ diff --git a/tts/piper/espeak-ng-data/tn_dict b/tts/piper/espeak-ng-data/tn_dict new file mode 100644 index 0000000..ffa3962 Binary files /dev/null and b/tts/piper/espeak-ng-data/tn_dict differ diff --git a/tts/piper/espeak-ng-data/tr_dict b/tts/piper/espeak-ng-data/tr_dict new file mode 100644 index 0000000..69d521f Binary files /dev/null and b/tts/piper/espeak-ng-data/tr_dict differ diff --git a/tts/piper/espeak-ng-data/tt_dict b/tts/piper/espeak-ng-data/tt_dict new file mode 100644 index 0000000..ef0b02b Binary files /dev/null and b/tts/piper/espeak-ng-data/tt_dict differ diff --git a/tts/piper/espeak-ng-data/ug_dict b/tts/piper/espeak-ng-data/ug_dict new file mode 100644 index 0000000..25f827f Binary files /dev/null and b/tts/piper/espeak-ng-data/ug_dict differ diff --git a/tts/piper/espeak-ng-data/uk_dict b/tts/piper/espeak-ng-data/uk_dict new file mode 100644 index 0000000..326dc44 Binary files /dev/null and b/tts/piper/espeak-ng-data/uk_dict differ diff --git a/tts/piper/espeak-ng-data/ur_dict b/tts/piper/espeak-ng-data/ur_dict new file mode 100644 index 0000000..cfc9693 Binary files /dev/null and b/tts/piper/espeak-ng-data/ur_dict differ diff --git a/tts/piper/espeak-ng-data/uz_dict b/tts/piper/espeak-ng-data/uz_dict new file mode 100644 index 0000000..c58fae5 Binary files /dev/null and b/tts/piper/espeak-ng-data/uz_dict differ diff --git a/tts/piper/espeak-ng-data/vi_dict b/tts/piper/espeak-ng-data/vi_dict new file mode 100644 index 0000000..b78717c Binary files /dev/null and b/tts/piper/espeak-ng-data/vi_dict differ diff --git a/tts/piper/espeak-ng-data/voices/!v/Alex b/tts/piper/espeak-ng-data/voices/!v/Alex new file mode 100644 index 0000000..10c6b6e --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Alex @@ -0,0 +1,10 @@ +language variant +name Alex + +voicing 70 +pitch 105 115 +flutter 0 + +formant 1 110 115 100 +formant 2 100 110 100 +formant 3 100 80 75 diff --git a/tts/piper/espeak-ng-data/voices/!v/Alicia b/tts/piper/espeak-ng-data/voices/!v/Alicia new file mode 100644 index 0000000..d33ed3a --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Alicia @@ -0,0 +1,22 @@ +language variant +name Alicia +gender female +pitch 180 275 +echo 40 50 +formant 0 115 115 110 +formant 1 130 160 120 +formant 2 150 110 150 +formant 3 135 150 100 +formant 4 120 120 120 +formant 5 120 120 120 +formant 6 100 110 105 +formant 7 100 110 160 +formant 8 200 120 100 +intonation 2 +voicing 38 +consonants 100 20 +roughness 1 +stressAdd 1 64 64 50 50 100 100 200 +stressAmp 12 12 20 20 12 12 20 20 +breathw 150 150 200 200 400 400 600 600 +breath 0 4 5 2 3 13 3 2 \ No newline at end of file diff --git a/tts/piper/espeak-ng-data/voices/!v/Andrea b/tts/piper/espeak-ng-data/voices/!v/Andrea new file mode 100644 index 0000000..39fb186 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Andrea @@ -0,0 +1,20 @@ +language variant +name Andrea +gender female + +pitch 200 265 +roughness 0 + +formant 0 100 100 100 +formant 1 110 100 80 +formant 2 110 80 80 +formant 3 115 110 80 +formant 4 115 80 100 +formant 5 95 50 100 +formant 6 0 0 0 +formant 7 120 100 100 +formant 8 110 100 100 +intonation 3 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 130 140 140 100 0 0 130 160 +voicing 150 \ No newline at end of file diff --git a/tts/piper/espeak-ng-data/voices/!v/Andy b/tts/piper/espeak-ng-data/voices/!v/Andy new file mode 100644 index 0000000..25582fb --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Andy @@ -0,0 +1,19 @@ +language variant +name Andy +gender Male + +pitch 85 110 + +flutter 0 +formant 0 80 80 80 80 +formant 1 100 100 100 120 +formant 2 100 88 100 +formant 3 0 0 0 +formant 4 80 80 80 +formant 5 80 80 80 +formant 6 0 0 0 +formant 7 0 0 0 +formant 8 0 0 0 +stressAdd 0 0 0 0 0 0 0 200 +stressAmp 35 35 35 35 35 35 35 35 35 + diff --git a/tts/piper/espeak-ng-data/voices/!v/Annie b/tts/piper/espeak-ng-data/voices/!v/Annie new file mode 100644 index 0000000..3c5b035 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Annie @@ -0,0 +1,17 @@ +language variant +name Annie +gender female +pitch 120 280 + +formant 0 105 125 120 +formant 1 120 140 120 +formant 2 120 150 140 +formant 3 130 150 130 +formant 4 120 120 110 +formant 5 120 120 110 +formant 6 120 140 130 +formant 7 120 140 130 +formant 8 120 140 130 +intonation 1 +voicing 30 +consonants 110 120 diff --git a/tts/piper/espeak-ng-data/voices/!v/AnxiousAndy b/tts/piper/espeak-ng-data/voices/!v/AnxiousAndy new file mode 100644 index 0000000..0ed1f35 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/AnxiousAndy @@ -0,0 +1,19 @@ +language variant +name anxiousAndy +gender Male + +pitch 115 110 + +flutter 0 +formant 0 80 80 80 80 +formant 1 100 100 100 120 +formant 2 100 100 100 +formant 3 0 0 0 +formant 4 0 0 0 +formant 5 100 100 100 +formant 6 100 100 100 +formant 7 100 100 100 +formant 8 100 100 100 +stressAdd 100 100 100 100 100 100 100 300 +stressAmp 35 35 35 35 35 35 35 35 35 + diff --git a/tts/piper/espeak-ng-data/voices/!v/Demonic b/tts/piper/espeak-ng-data/voices/!v/Demonic new file mode 100644 index 0000000..131fe46 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Demonic @@ -0,0 +1,65 @@ +##Ten en cuenta que los 2 signos de número en este archivo tienen explicaciones de las comfiguraciones que puede aplicar y cómo comfigurarlas +## Language Establece el idioma de la voz. Esta opción es necesaria para cualquier comfiguración que realices +##La siguiente línea es una configuración que puede cambiar. Sin embargo, si no conoce los códigos de idioma, puede ser mejor dejar la configuración tal y como está. +language variant + +## La configuración de nombre es el nombre que aparecerá en la configuración de voz en el cuadro combinado de variante. +##La siguiente línea es una opción que puede cambiar +name Demonic +##La siguiente línea establece el género de la voz. Male or Female (hombre o mujer) +##La siguiente línea es una opción que puede cambiar +gender male + +flutter 5 +stressAmp 20 18 20 20 20 22 22 22 +##Las opciones de formantes +## Formant 0 es usado para dar una baja frecuencia a los sonnidos +## Los tres números son frecuencia, fuerza y ancho, en orden. Ten en cuenta que los números están separados por espacios +##La siguiente línea es una opción que puede cambiar +formant 0 100 100 100 + +# Formant 1, 2, y 3 son las 3 formantes estándar para definir las vocales. +##Las siguientes 3 líneas son opciones que puedes cambiar +formant 1 70 100 100 +formant 2 80 100 90 +formant 3 80 160 90 + +# Formants 4 y 5 afectan a f3. Esto afectará la calidad de la voz. +##Las siguientes 2 líneas son comfiguraciones que puede cambiar. +formant 4 80 85 +formant 5 100 100 80 + +## Formantes 6, 7 y 8 son opciones que te ofrecen un sonido más claro de las vocales +##Las siguientes 3 líneas son opciones que puedes cambiar +formant 6 80 80 100 +formant 7 130 130 110 +formant 8 120 120 150 + +##Intonation afecta el ascenso y la caída de la voz +## Las opciones son: 1 predeterminado, 2 entonación media, 3 entonación media y no afecta a las comas, 4 al final de la oración o punto aumenta el tono de la voz. +##La siguiente línea es una opción que puedes cambiar. +intonation 10 + +# Establecer el rango de tono. El primer número le da un tono base a la voz (valor en hz). El segundo número controla el rango de tonos usado por la voz. Poniéndolo igual +# si los 2 números son iguales, la voz será monótona. Por defecto los ajustes son 82 y 118 +pitch 43 120 +## La configuración del tono. El primer número en la línea de configuración, 600, es la configuración de frecuencia para la cantidad de graves en la voz. +## El segundo número en la línea de tono es el volumen de la frecuencia de graves. Puede configurarlo de 0 a 255, siendo 0 la menor cantidad, 255 la mayor. +##El tercer número en la línea de tono, 1200, es la frecuencia de rango medio. El cuarto número en la línea es la configuración para cambiar el volumen de la frecuencia de rango medio. +##0 es la menor cantidad y 255 es la mayor. +## El quinto número en la línea de tono, 2000, es la frecuencia de agudos. El sexto número es el volumen de la frecuencia de agudos. 0 es el mínimo y 255 es el máximo. +## Notará que las 3 frecuencias están configuradas en 255. +###La siguiente línea es una opción que puedes cambiar. +tone 100 255 1200 255 1500 255 +echo 8 10000 +roughness 3 +breath 20 5 2 10 5 0 27 100 +breathw 255 255 60 180 160 255 255 255 +consonants 194 255 +voicing 65 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 250 350 700 500 450 290 100 225 +stressAmp 16 16 24 24 16 16 20 24 +##Este archivo no incluye todas las configuraciones que se pueden usar para modificar una voz E Speak. Su objetivo es familiarizarlo con lo que hace la configuración. +##Sin envargo puedes visitar la página http://espeak.sourceforge.net/voices.html y consultar más información acerca de cómo agregar o cambiar otras configuraciones. +## Espero que te haya servido esta ayuda, y que te hayas divertido. diff --git a/tts/piper/espeak-ng-data/voices/!v/Denis b/tts/piper/espeak-ng-data/voices/!v/Denis new file mode 100644 index 0000000..23c7fb7 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Denis @@ -0,0 +1,19 @@ +language variant +name Denis +gender male 35 +pitch 80 115 +flutter 0 +roughness 0 + + +formant 0 100 160 160 +formant 1 95 95 95 +formant 2 100 100 100 +formant 3 90 90 90 +formant 4 40 40 40 +formant 5 80 80 80 +formant 6 10 10 10 +formant 7 10 10 10 +formant 8 10 10 10 +voicing 40 +consonants 80 80 diff --git a/tts/piper/espeak-ng-data/voices/!v/Diogo b/tts/piper/espeak-ng-data/voices/!v/Diogo new file mode 100644 index 0000000..1e51396 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Diogo @@ -0,0 +1,22 @@ +language variant +name Diogo +gender male 25 +pitch 82 122 +echo 0 0 +flutter 0 +roughness 0 +stressAmp 20 18 20 20 20 22 22 22 + + +formant 0 105 200 140 +formant 1 95 150 120 +formant 2 100 120 140 +formant 3 95 95 140 +formant 4 30 30 30 -100 +formant 5 90 90 90 +formant 6 110 60 65 +formant 7 100 0 100 +formant 8 100 0 100 +voicing 35 +consonants 60 40 +tone 60 250 140 100 1000 50 3500 35 diff --git a/tts/piper/espeak-ng-data/voices/!v/Gene b/tts/piper/espeak-ng-data/voices/!v/Gene new file mode 100644 index 0000000..3db343b --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Gene @@ -0,0 +1,17 @@ +language variant +name Gene + +pitch 80 110 + +formant 0 120 120 120 +formant 1 90 100 110 +formant 2 100 100 95 +formant 3 90 100 100 +formant 4 90 100 110 +formant 5 90 110 110 +formant 6 100 70 100 +formant 7 100 70 100 +formant 8 100 80 100 +voicing 120 +consonants 50 110 + diff --git a/tts/piper/espeak-ng-data/voices/!v/Gene2 b/tts/piper/espeak-ng-data/voices/!v/Gene2 new file mode 100644 index 0000000..20e8f6f --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Gene2 @@ -0,0 +1,17 @@ +language variant +name Gene2 + +pitch 100 130 + +formant 0 120 120 120 +formant 1 90 100 110 +formant 2 100 100 95 +formant 3 90 100 100 +formant 4 90 100 110 +formant 5 90 110 110 +formant 6 100 70 100 +formant 7 100 70 100 +formant 8 100 80 100 +voicing 120 +consonants 50 110 + diff --git a/tts/piper/espeak-ng-data/voices/!v/Henrique b/tts/piper/espeak-ng-data/voices/!v/Henrique new file mode 100644 index 0000000..d3fa3d7 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Henrique @@ -0,0 +1,22 @@ +language variant +name Henrique +gender male 25 +pitch 70 130 +echo 0 0 +flutter 0 +roughness 0 +stressAmp 20 18 20 20 20 22 22 22 + + +formant 0 105 200 140 +formant 1 95 150 120 +formant 2 100 120 140 +formant 3 95 95 140 +formant 4 30 30 30 -100 +formant 5 90 90 90 +formant 6 110 60 65 +formant 7 100 0 100 +formant 8 100 0 100 +voicing 35 +consonants 60 40 +tone 70 250 230 80 1100 30 3500 40 diff --git a/tts/piper/espeak-ng-data/voices/!v/Hugo b/tts/piper/espeak-ng-data/voices/!v/Hugo new file mode 100644 index 0000000..fe79235 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Hugo @@ -0,0 +1,22 @@ +language variant +name Hugo +gender male 25 +pitch 70 130 +echo 0 0 +flutter 0 +roughness 0 +stressAmp 20 18 20 20 20 22 22 22 + + +formant 0 105 200 140 +formant 1 95 150 120 +formant 2 100 120 140 +formant 3 95 95 140 +formant 4 30 30 30 -100 +formant 5 90 90 90 +formant 6 110 60 65 +formant 7 100 0 100 +formant 8 100 0 100 +voicing 35 +consonants 60 40 +tone 400 160 1100 90 3500 90 150 35 diff --git a/tts/piper/espeak-ng-data/voices/!v/Jacky b/tts/piper/espeak-ng-data/voices/!v/Jacky new file mode 100644 index 0000000..e0884ed --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Jacky @@ -0,0 +1,17 @@ +language variant +name Jacky + +pitch 85 130 + +formant 0 150 155 100 +formant 1 90 155 70 +formant 2 95 70 64 +formant 3 15 20 30 +formant 4 20 30 40 +formant 5 65 20 65 +formant 6 70 80 100 +formant 7 20 80 100 +formant 8 100 95 80 +voicing 135 +consonants 110 + diff --git a/tts/piper/espeak-ng-data/voices/!v/Lee b/tts/piper/espeak-ng-data/voices/!v/Lee new file mode 100644 index 0000000..e0194fa --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Lee @@ -0,0 +1,20 @@ +language variant +name Lee +gender Male + +#echo 230 30 +pitch 85 110 + +flutter 0 +formant 0 80 80 80 80 +formant 1 80 80 100 100 +formant 2 80 80 80 +formant 3 9 9 9 +formant 4 290 290 +formant 5 130 0 0 +formant 6 90 90 90 +formant 7 90 90 90 +formant 8 90 90 90 +stressAdd 0 0 0 200 0 0 0 100 +stressAmp 30 30 30 30 30 30 30 30 30 + diff --git a/tts/piper/espeak-ng-data/voices/!v/Marco b/tts/piper/espeak-ng-data/voices/!v/Marco new file mode 100644 index 0000000..455cb26 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Marco @@ -0,0 +1,23 @@ +language variant +name Marco +gender male 30 +intonation 1 +pitch 100 152 +echo 50 80 +flutter 2 +roughness 0 +stressAmp 25 25 24 20 38 31 39 27 +stressAdd 250 125 250 250 225 145 50 256 + +formant 0 100 120 130 +formant 1 75 180 170 +formant 2 92 120 110 +formant 3 140 120 110 +formant 4 10 20 20 -50 +formant 5 110 70 20 +formant 6 140 100 98 +formant 7 130 120 115 +formant 8 105 120 108 +voicing 38 +consonants 90 140 +tone 420 150 1200 135 3000 70 4700 40 diff --git a/tts/piper/espeak-ng-data/voices/!v/Mario b/tts/piper/espeak-ng-data/voices/!v/Mario new file mode 100644 index 0000000..3ace369 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Mario @@ -0,0 +1,17 @@ +language variant +name Mario + +pitch 75 125 + +formant 0 100 111 95 +formant 1 100 111 60 +formant 2 95 90 55 +formant 3 100 50 65 +formant 4 69 65 65 +formant 5 79 60 75 +formant 6 89 60 75 +formant 7 99 0 100 +formant 8 109 0 100 +voicing 135 +consonants 115 120 + diff --git a/tts/piper/espeak-ng-data/voices/!v/Michael b/tts/piper/espeak-ng-data/voices/!v/Michael new file mode 100644 index 0000000..e155ea8 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Michael @@ -0,0 +1,17 @@ +language variant +name Michael + +pitch 75 125 + +formant 0 105 111 95 +formant 1 85 111 60 +formant 2 95 90 55 +formant 3 59 50 65 +formant 4 69 65 65 +formant 5 79 60 75 +formant 6 89 60 75 +formant 7 99 0 100 +formant 8 109 0 100 +voicing 135 +consonants 115 120 + diff --git a/tts/piper/espeak-ng-data/voices/!v/Mike b/tts/piper/espeak-ng-data/voices/!v/Mike new file mode 100644 index 0000000..93ea4d8 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Mike @@ -0,0 +1,7 @@ +language variant +name Mike +voicing 70 +formant 1 96 97 100 +formant 2 96 97 100 +formant 5 95 103 100 +pitch 67 107 diff --git a/tts/piper/espeak-ng-data/voices/!v/Mr serious b/tts/piper/espeak-ng-data/voices/!v/Mr serious new file mode 100644 index 0000000..4623c84 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Mr serious @@ -0,0 +1,50 @@ +##Please note the 2 number signs, or pound signs in this file are for comments to help you to understand what the settings are and how to set them. +## Language sets the language of your voice. This setting is required for every voice that you make. +##The next line is a setting you can change. However if you don't know the language codes it may be best to leave the setting as it is. +language variant + +## The name setting is the name that will show up in the voice settings in the variant combo box. +##The next line is a setting you can change +name Mr_Serious + +##The formant settings +## Formant 0 is used to give a low frequency component to the sounds. +## The three numbers are frequency, strength, and Width, in that order. Please note, the numbers are seperated by a space. +##The next line is a setting you can change +formant 0 100 100 100 + +# Formants 1,2, and 3 are the standard three formants which define vowels. +##The next 3 lines are settings you can change +formant 1 100 100 100 +formant 2 100 100 100 +formant 3 87 100 100 + +# Formants 4,5 are higher than F3. They affect the quality of the voice. +##The next 2 lines are settings that you can change. +formant 4 100 100 100 +formant 5 100 100 100 + +## Formants 6, 7, and 8 are weak, high frequency, additions to vowels to give a clearer sound. +##The next 3 lines are settings that you can change. +formant 6 100 100 100 +formant 7 100 100 100 +formant 8 100 100 100 + +##Intonation affects the rise and fall of the voice +## The settings are 1 default, 2 less intonation, 3 less intonation and commas do not raise the pitch, 4 the pitch rises at the end of a sentence rather than falling. +##The next line is a setting you can change. +intonation 1 + +# Setting the pitch range. The first number gives a base pitch to the voice (value in Hertz). The second number controls the range of pitches used by the voice. Setting it equal +# to the first number will give a monotone sounding voice. The default values are 82 and 118. +pitch 82 118 +## The tone setting. The first number on the setting line, 600, is the frequency setting for the amount of bass in the voice. +## The second number on the tone line is the volume of the bass frequency. You can set it from 0 to 255, 0 being the least amount, 255 being the most. +##The third number on the tone line, 1200, is the mid range frequency. The fourth number on the line is the setting to change the volume of the mid range frequency. +##0 being the least amount and 255 being the maximum. +## The fifth number on the tone line, 2000, is the treble frequency. The sixth number is the volume of the treble frequency. 0 is the minimum and 255 is the maximum. +## You will notice that all 3 frequencies are set to 255. +##The next line is a setting that you can change. +tone 600 255 1200 255 2000 255 +##This file does not include all of the settings that can be used to modify an E Speak voice. It is intended to get you familiar with what the settings do. +##However, you can go to http://espeak.sourceforge.net/voices.html and read further information about other settings that can be added and changed. I hope this helps, and Have fun. diff --git a/tts/piper/espeak-ng-data/voices/!v/Nguyen b/tts/piper/espeak-ng-data/voices/!v/Nguyen new file mode 100644 index 0000000..50c72fc --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Nguyen @@ -0,0 +1,16 @@ +language variant +name Nguyen + +pitch 95 175 + +formant 0 100 125 100 +formant 1 96 90 80 +formant 2 97 70 90 +formant 3 97 60 90 +formant 4 97 60 90 +formant 5 75 50 90 +formant 6 90 50 100 +formant 7 100 50 100 +formant 8 100 50 100 + +tone 100 200 600 150 800 100 2400 80 3600 95 5400 100 diff --git a/tts/piper/espeak-ng-data/voices/!v/Reed b/tts/piper/espeak-ng-data/voices/!v/Reed new file mode 100644 index 0000000..1ccdf5d --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Reed @@ -0,0 +1,15 @@ +language variant +name Reed +klatt 6 +consonants 85 85 +voicing 130 +breath 45 + +pitch 85 135 + +formant 1 72 100 90 90 +formant 2 83 100 75 180 +formant 3 98 100 100 90 +formant 4 98 100 90 +formant 5 100 100 90 + diff --git a/tts/piper/espeak-ng-data/voices/!v/RicishayMax b/tts/piper/espeak-ng-data/voices/!v/RicishayMax new file mode 100644 index 0000000..b269b64 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/RicishayMax @@ -0,0 +1,16 @@ +language variant +name RicishayMax +echo 100 10000 + +formant 0 90 120 100 +formant 1 100 100 75 +formant 2 100 100 75 +formant 3 100 80 75 +formant 4 100 80 75 +formant 5 100 80 75 +formant 6 100 0 75 +formant 7 100 0 75 +formant 8 100 0 75 + + + diff --git a/tts/piper/espeak-ng-data/voices/!v/RicishayMax2 b/tts/piper/espeak-ng-data/voices/!v/RicishayMax2 new file mode 100644 index 0000000..6585134 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/RicishayMax2 @@ -0,0 +1,25 @@ +language variant +name RicishayMax2 +echo 150 500 + +formant 0 90 120 100 +formant 1 100 100 75 +formant 2 100 100 75 +formant 3 100 80 75 +formant 4 100 80 75 +formant 5 100 80 75 +formant 6 100 0 75 +formant 7 100 0 75 +formant 8 100 0 75 + + +roughness 5 + +intonation 10 +voicing 150 +consonants 110 120 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 130 140 140 100 0 0 130 160 +stressAmp 16 16 24 24 16 16 20 24 + +tone 100 255 600 70 1200 22 2000 66 3000 12 diff --git a/tts/piper/espeak-ng-data/voices/!v/RicishayMax3 b/tts/piper/espeak-ng-data/voices/!v/RicishayMax3 new file mode 100644 index 0000000..f481002 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/RicishayMax3 @@ -0,0 +1,25 @@ +language variant +name RicishayMax3 +echo 200 500 + +formant 0 90 120 100 +formant 1 100 100 75 +formant 2 100 100 75 +formant 3 100 80 75 +formant 4 100 80 75 +formant 5 100 80 75 +formant 6 100 0 75 +formant 7 100 0 75 +formant 8 100 0 75 + + +roughness 5 + +intonation 10 +voicing 150 +consonants 110 120 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 130 140 140 100 0 0 130 160 +stressAmp 16 16 24 24 16 16 20 24 + +tone 100 255 600 70 1200 22 2000 66 3000 12 diff --git a/tts/piper/espeak-ng-data/voices/!v/Storm b/tts/piper/espeak-ng-data/voices/!v/Storm new file mode 100644 index 0000000..6fd53bb --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Storm @@ -0,0 +1,23 @@ +language variant +language en-us +name Storm +gender male +formant 0 100 100 100 +formant 1 95 95 95 +formant 2 95 95 95 +formant 3 95 95 95 +formant 4 70 70 70 +formant 5 70 70 70 +formant 6 25 25 25 +formant 7 25 25 25 +formant 8 25 25 25 +breath 0 0 0 0 0 0 0 0 +consonants 100 +echo 0 0 +flutter 0 +intonation 3 +pitch 60 100 +roughness 0 +stressAdd 5 5 3 3 0 0 -15 -15 +tone 500 255 1500 255 2500 255 +voicing 100 diff --git a/tts/piper/espeak-ng-data/voices/!v/Tweaky b/tts/piper/espeak-ng-data/voices/!v/Tweaky new file mode 100644 index 0000000..f391d13 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/Tweaky @@ -0,0 +1,50 @@ +##Please note the 2 number signs, or pound signs in this file are for comments to help you to understand what the settings are and how to set them. +## Language sets the language of your voice. This setting is required for every voice that you make. +##The next line is a setting you can change. However if you don't know the language codes it may be best to leave the setting as it is. +language variant + +## The name setting is the name that will show up in the voice settings in the variant combo box. +##The next line is a setting you can change +name Tweaky + +##The formant settings +## Formant 0 is used to give a low frequency component to the sounds. +## The three numbers are frequency, strength, and Width, in that order. Please note, the numbers are seperated by a space. +##The next line is a setting you can change +formant 0 100 100 100 + +# Formants 1,2, and 3 are the standard three formants which define vowels. +##The next 3 lines are settings you can change +formant 1 100 100 100 +formant 2 100 100 100 +formant 3 200 100 100 + +# Formants 4,5 are higher than F3. They affect the quality of the voice. +##The next 2 lines are settings that you can change. +formant 4 100 100 100 +formant 5 100 100 100 + +## Formants 6, 7, and 8 are weak, high frequency, additions to vowels to give a clearer sound. +##The next 3 lines are settings that you can change. +formant 6 100 100 100 +formant 7 100 100 100 +formant 8 100 100 100 + +##Intonation affects the rise and fall of the voice +## The settings are 1 default, 2 less intonation, 3 less intonation and commas do not raise the pitch, 4 the pitch rises at the end of a sentence rather than falling. +##The next line is a setting you can change. +intonation 1 + +# Setting the pitch range. The first number gives a base pitch to the voice (value in Hertz). The second number controls the range of pitches used by the voice. Setting it equal +# to the first number will give a monotone sounding voice. The default values are 82 and 118. +pitch 82 118 +## The tone setting. The first number on the setting line, 600, is the frequency setting for the amount of bass in the voice. +## The second number on the tone line is the volume of the bass frequency. You can set it from 0 to 255, 0 being the least amount, 255 being the most. +##The third number on the tone line, 1200, is the mid range frequency. The fourth number on the line is the setting to change the volume of the mid range frequency. +##0 being the least amount and 255 being the maximum. +## The fifth number on the tone line, 2000, is the treble frequency. The sixth number is the volume of the treble frequency. 0 is the minimum and 255 is the maximum. +## You will notice that all 3 frequencies are set to 255. +##The next line is a setting that you can change. +tone 600 255 1200 255 2000 255 +##This file does not include all of the settings that can be used to modify an E Speak voice. It is intended to get you familiar with what the settings do. +##However, you can go to http://espeak.sourceforge.net/voices.html and read further information about other settings that can be added and changed. I hope this helps, and Have fun. diff --git a/tts/piper/espeak-ng-data/voices/!v/UniRobot b/tts/piper/espeak-ng-data/voices/!v/UniRobot new file mode 100644 index 0000000..f889278 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/UniRobot @@ -0,0 +1,19 @@ +language variant +name UniversalRobot +gender male +klatt 4 +pitch 100 160 +echo 10 10000 +formant 1 75 120 135 +formant 2 90 50 140 +formant 3 70 85 95 +formant 4 150 60 80 +formant 5 100 85 80 +formant 6 112 100 80 +formant 7 110 95 100 +formant 8 105 110 100 +consonants 125 100 +tone 530 250 770 100 215 225 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 120 130 130 90 0 0 120 120 +stressAmp 16 16 24 24 16 16 20 24 diff --git a/tts/piper/espeak-ng-data/voices/!v/adam b/tts/piper/espeak-ng-data/voices/!v/adam new file mode 100644 index 0000000..a9f3d2a --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/adam @@ -0,0 +1,6 @@ +language variant +name Adam +klatt 6 +consonants 85 85 + +formant 1 100 100 130 diff --git a/tts/piper/espeak-ng-data/voices/!v/anika b/tts/piper/espeak-ng-data/voices/!v/anika new file mode 100644 index 0000000..2413c90 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/anika @@ -0,0 +1,25 @@ +language variant +name anika +gender female +pitch 200 300 +flutter 6 +stressAmp 20 18 20 20 20 22 22 22 + +roughness 0 + +formant 0 105 200 140 +formant 1 95 150 120 +formant 2 100 120 140 +formant 3 95 95 140 +formant 4 120 120 110 +formant 5 120 120 110 +formant 6 110 60 65 +formant 7 100 0 100 +formant 8 100 0 100 +intonation 10 +voicing 30 +consonants 60 40 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 130 140 140 100 0 0 130 160 +stressAmp 16 16 24 24 16 16 20 24 +tone 100 255 600 70 1200 22 2000 66 3000 12 diff --git a/tts/piper/espeak-ng-data/voices/!v/anikaRobot b/tts/piper/espeak-ng-data/voices/!v/anikaRobot new file mode 100644 index 0000000..791d2bb --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/anikaRobot @@ -0,0 +1,26 @@ +language variant +name anikaRobot +gender female +pitch 200 300 +flutter 1 +stressAmp 20 18 20 20 20 22 22 22 +echo 10 10000 + +roughness 0 + +formant 0 105 200 140 +formant 1 95 150 120 +formant 2 100 120 140 +formant 3 95 95 140 +formant 4 120 120 110 +formant 5 120 120 110 +formant 6 110 60 65 +formant 7 100 0 100 +formant 8 100 0 100 +intonation 10 +voicing 30 +consonants 60 40 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 130 140 140 100 0 0 130 160 +stressAmp 16 16 24 24 16 16 20 24 +tone 100 255 600 70 1200 22 2000 66 3000 12 diff --git a/tts/piper/espeak-ng-data/voices/!v/announcer b/tts/piper/espeak-ng-data/voices/!v/announcer new file mode 100644 index 0000000..71e0795 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/announcer @@ -0,0 +1,17 @@ +name Half-LifeAnnouncementSystem +language variant +pitch 37 83 +klatt 4 + +formant 1 88 100 100 0 +formant 2 96 100 100 0 +formant 3 98 100 100 0 +formant 4 96 100 100 0 +formant 5 100 100 100 0 +formant 6 100 100 100 0 +formant 7 100 100 100 0 +formant 8 100 100 100 0 + +voicing 70 +consonants 70 70 +echo 154 26 diff --git a/tts/piper/espeak-ng-data/voices/!v/antonio b/tts/piper/espeak-ng-data/voices/!v/antonio new file mode 100644 index 0000000..a41b681 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/antonio @@ -0,0 +1,21 @@ +language variant +name Antonio +gender male + +pitch 82 128 +roughness 0 + +formant 0 100 150 90 +formant 1 90 130 90 +formant 2 95 120 80 +formant 3 100 50 80 +formant 4 100 40 80 +formant 5 90 70 80 +formant 6 0 0 0 +formant 7 100 100 100 +formant 8 100 100 100 +voicing 150 +tone 600 255 1200 255 2000 80 +intonation 3 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 130 140 140 100 0 0 130 160 diff --git a/tts/piper/espeak-ng-data/voices/!v/aunty b/tts/piper/espeak-ng-data/voices/!v/aunty new file mode 100644 index 0000000..7d721ee --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/aunty @@ -0,0 +1,19 @@ +language variant +name Auntie +gender female +pitch 204 176 +flutter 12 + +formant 0 88 85 154 +formant 1 115 80 160 -20 +formant 2 130 75 150 -200 +formant 3 123 75 150 +formant 4 125 80 150 +formant 5 125 80 150 +formant 6 110 80 150 +formant 7 110 75 150 +formant 8 110 75 150 + +stressAdd -20 -20 -20 -20 0 0 20 120 +stressAmp 18 16 20 20 20 20 20 20 + diff --git a/tts/piper/espeak-ng-data/voices/!v/belinda b/tts/piper/espeak-ng-data/voices/!v/belinda new file mode 100644 index 0000000..9a9e42b --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/belinda @@ -0,0 +1,20 @@ +language variant +name Belinda +gender female + +pitch 200 247 +flutter 3 + +formant 0 88 85 154 +formant 1 135 58 169 -30 +formant 2 120 70 150 -260 +formant 3 120 39 150 +formant 4 125 57 80 +formant 5 125 80 150 +formant 6 110 80 150 +formant 7 110 75 150 +formant 8 110 75 150 + +stressAdd -20 -20 -20 -20 0 3 20 12 +stressAmp 18 16 20 20 10 20 27 20 + diff --git a/tts/piper/espeak-ng-data/voices/!v/benjamin b/tts/piper/espeak-ng-data/voices/!v/benjamin new file mode 100644 index 0000000..c300643 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/benjamin @@ -0,0 +1,11 @@ +language variant +name Benjamin +klatt 6 +consonants 70 70 + +formant 1 101 100 130 +formant 2 102 100 100 +formant 3 100 100 100 +formant 4 100 100 100 470 +formant 5 100 100 100 350 +formant 6 100 100 100 100 diff --git a/tts/piper/espeak-ng-data/voices/!v/boris b/tts/piper/espeak-ng-data/voices/!v/boris new file mode 100644 index 0000000..02e03ce --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/boris @@ -0,0 +1,15 @@ +language variant +name Boris + +formant 0 47 120 100 +formant 1 100 90 75 +formant 2 104 100 75 +formant 3 57 80 75 +formant 4 104 80 75 +formant 5 107 80 75 +formant 6 68 0 75 +formant 7 105 0 75 +formant 8 105 0 75 + + + diff --git a/tts/piper/espeak-ng-data/voices/!v/caleb b/tts/piper/espeak-ng-data/voices/!v/caleb new file mode 100644 index 0000000..9c8a5b9 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/caleb @@ -0,0 +1,5 @@ +language variant +name Caleb +klatt 6 +breath 100 +voicing 0 diff --git a/tts/piper/espeak-ng-data/voices/!v/croak b/tts/piper/espeak-ng-data/voices/!v/croak new file mode 100644 index 0000000..ae76a4c --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/croak @@ -0,0 +1,11 @@ +language variant +name croak +gender male 70 + +pitch 85 117 +flutter 20 + +formant 0 100 80 110 + + + diff --git a/tts/piper/espeak-ng-data/voices/!v/david b/tts/piper/espeak-ng-data/voices/!v/david new file mode 100644 index 0000000..7dc75dd --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/david @@ -0,0 +1,8 @@ +language variant +name David +klatt 6 +pitch 62 89 + +formant 1 75 100 100 +formant 2 85 100 100 +formant 3 85 100 100 diff --git a/tts/piper/espeak-ng-data/voices/!v/ed b/tts/piper/espeak-ng-data/voices/!v/ed new file mode 100644 index 0000000..7f293fc --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/ed @@ -0,0 +1,17 @@ +language variant +name Ed + +pitch 90 145 + +formant 0 110 120 200 5 +formant 1 102 100 80 +formant 2 101 120 100 +formant 3 100 80 75 +formant 4 150 30 80 +formant 5 95 95 155 +formant 6 167 100 75 +formant 7 100 200 75 +formant 8 60 200 95 +consonants 55 80 +voicing 100 +tone 650 250 1000 130 240 255 \ No newline at end of file diff --git a/tts/piper/espeak-ng-data/voices/!v/edward b/tts/piper/espeak-ng-data/voices/!v/edward new file mode 100644 index 0000000..303f505 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/edward @@ -0,0 +1,10 @@ +language variant +name Edward +klatt 5 +voicing 100 +consonants 70 80 + +formant 1 92 100 130 +formant 2 103 100 80 +formant 3 103 100 70 +formant 4 114 100 60 diff --git a/tts/piper/espeak-ng-data/voices/!v/edward2 b/tts/piper/espeak-ng-data/voices/!v/edward2 new file mode 100644 index 0000000..abee6aa --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/edward2 @@ -0,0 +1,10 @@ +language variant +name Edward2 +klatt 6 +voicing 100 +consonants 70 80 + +formant 1 92 100 130 +formant 2 103 100 80 +formant 3 103 100 70 +formant 4 114 100 60 diff --git a/tts/piper/espeak-ng-data/voices/!v/f1 b/tts/piper/espeak-ng-data/voices/!v/f1 new file mode 100644 index 0000000..8f03a73 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/f1 @@ -0,0 +1,18 @@ +language variant +name female1 +gender female 70 + +pitch 140 200 +flutter 8 +roughness 4 +formant 0 115 80 150 +formant 1 120 80 180 +formant 2 100 70 150 150 +formant 3 115 70 150 +formant 4 110 80 150 +formant 5 110 90 150 +formant 6 105 80 150 +formant 7 110 70 150 +formant 8 110 70 150 + +stressAdd -10 -10 -20 -20 0 0 40 60 diff --git a/tts/piper/espeak-ng-data/voices/!v/f2 b/tts/piper/espeak-ng-data/voices/!v/f2 new file mode 100644 index 0000000..4122d96 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/f2 @@ -0,0 +1,21 @@ +language variant +name female2 +gender female + +pitch 142 220 +roughness 3 + +formant 0 105 80 150 +formant 1 110 80 160 +formant 2 110 70 150 +formant 3 110 70 150 +formant 4 115 80 150 +formant 5 115 80 150 +formant 6 110 70 150 +formant 7 110 70 150 +formant 8 110 70 150 + +stressAdd 0 0 -10 -10 0 0 10 40 +breath 0 2 3 3 3 3 3 2 +echo 140 10 +consonants 125 125 diff --git a/tts/piper/espeak-ng-data/voices/!v/f3 b/tts/piper/espeak-ng-data/voices/!v/f3 new file mode 100644 index 0000000..92a1582 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/f3 @@ -0,0 +1,22 @@ +language variant +name female3 +gender female + +pitch 140 240 +formant 0 105 80 150 +formant 1 120 75 150 -50 +formant 2 135 70 150 -250 +formant 3 125 80 150 +formant 4 125 80 150 +formant 5 125 80 150 +formant 6 120 70 150 +formant 7 110 70 150 +formant 8 110 70 150 + +stressAmp 18 18 20 20 20 20 20 20 +//breath 0 2 4 4 4 4 4 4 +breath 0 2 3 3 3 3 3 2 +echo 120 10 +roughness 4 + + diff --git a/tts/piper/espeak-ng-data/voices/!v/f4 b/tts/piper/espeak-ng-data/voices/!v/f4 new file mode 100644 index 0000000..52c5ac9 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/f4 @@ -0,0 +1,18 @@ +language variant +name female4 +gender female + +echo 130 15 +pitch 142 200 +formant 0 120 80 150 +formant 1 115 80 160 -20 +formant 2 130 75 150 -200 +formant 3 123 75 150 +formant 4 125 80 150 +formant 5 125 80 150 +formant 6 110 80 150 +formant 7 110 75 150 +formant 8 110 75 150 + +stressAdd -20 -20 -20 -20 0 0 20 120 +stressAmp 18 16 20 20 20 20 20 20 diff --git a/tts/piper/espeak-ng-data/voices/!v/f5 b/tts/piper/espeak-ng-data/voices/!v/f5 new file mode 100644 index 0000000..7fa4f88 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/f5 @@ -0,0 +1,23 @@ +language variant +name female5 +gender female + +pitch 160 228 +roughness 0 + +formant 0 105 80 150 +formant 1 110 80 160 +formant 2 110 70 150 +formant 3 110 70 150 +formant 4 115 80 200 +formant 5 115 80 100 +formant 6 110 70 150 +formant 7 110 70 100 +formant 8 110 70 150 + +stressAdd 0 0 -10 -10 0 0 10 40 +breath 0 4 6 6 6 6 0 10 +echo 140 10 +voicing 75 +consonants 150 150 +breathw 150 150 200 200 400 400 600 600 diff --git a/tts/piper/espeak-ng-data/voices/!v/fast b/tts/piper/espeak-ng-data/voices/!v/fast new file mode 100644 index 0000000..a2c3da2 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/fast @@ -0,0 +1,7 @@ +language variant +name fast_test + +// Try decreasing these values to make eSpeak's fastest speed faster. +// This is currently unstable. + +fast_test2 15 diff --git a/tts/piper/espeak-ng-data/voices/!v/grandma b/tts/piper/espeak-ng-data/voices/!v/grandma new file mode 100644 index 0000000..395ebe2 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/grandma @@ -0,0 +1,17 @@ +language variant + +name grandma +gender female 90 +pitch 120 230 + +flutter 20 +formant 0 105 150 150 +formant 1 100 80 100 +formant 2 105 105 105 +formant 3 80 80 80 +formant 4 60 60 60 +formant 5 90 90 90 +formant 6 10 10 10 +formant 7 10 10 10 +formant 8 20 20 20 +voicing 50 \ No newline at end of file diff --git a/tts/piper/espeak-ng-data/voices/!v/grandpa b/tts/piper/espeak-ng-data/voices/!v/grandpa new file mode 100644 index 0000000..41a870c --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/grandpa @@ -0,0 +1,14 @@ +language variant +name grandpa +pitch 80 120 +flutter 20 +formant 0 100 100 100 +formant 1 100 100 100 +formant 2 100 100 100 +formant 3 100 100 100 +formant 4 100 100 100 +formant 5 100 100 100 +formant 6 10 10 10 +formant 7 10 10 10 +formant 8 10 10 10 +intonation 1 diff --git a/tts/piper/espeak-ng-data/voices/!v/gustave b/tts/piper/espeak-ng-data/voices/!v/gustave new file mode 100644 index 0000000..ce1d71b --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/gustave @@ -0,0 +1,17 @@ +language variant +name Gustave + +pitch 80 123 + +formant 0 85 141 135 +formant 1 77 131 45 +formant 2 92 70 55 +formant 3 59 50 65 +formant 4 69 65 65 +formant 5 79 60 75 +formant 6 89 60 75 +formant 7 99 0 100 +formant 8 109 0 100 +voicing 135 +consonants 115 120 + diff --git a/tts/piper/espeak-ng-data/voices/!v/ian b/tts/piper/espeak-ng-data/voices/!v/ian new file mode 100644 index 0000000..3e3d409 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/ian @@ -0,0 +1,51 @@ +##Please note the 2 number signs, or pound signs in this file are for comments to help you to understand what the settings are and how to set them. +## Language sets the language of your voice. This setting is required for every voice that you make. +##The next line is a setting you can change. However if you don't know the language codes it may be best to leave the setting as it is. +language variant + +## The name setting is the name that will show up in the voice settings in the variant combo box. +##The next line is a setting you can change +name Ian + +##The formant settings +## Formant 0 is used to give a low frequency component to the sounds. +## The three numbers are frequency, strength, and Width, in that order. Please note, the numbers are seperated by a space. +##The next line is a setting you can change +formant 0 20 120 50 + +# Formants 1,2, and 3 are the standard three formants which define vowels. +##The next 3 lines are settings you can change +formant 1 80 80 80 +formant 2 80 80 80 +formant 3 80 80 80 + +# Formants 4,5 are higher than F3. They affect the quality of the voice. +##The next 2 lines are settings that you can change. +formant 4 50 50 50 +formant 5 50 50 50 + +## Formants 6, 7, and 8 are weak, high frequency, additions to vowels to give a clearer sound. +##The next 3 lines are settings that you can change. +formant 6 100 100 100 +formant 7 200 50 200 +formant 8 200 50 200 + +##Intonation affects the rise and fall of the voice +## The settings are 1 default, 2 less intonation, 3 less intonation and commas do not raise the pitch, 4 the pitch rises at the end of a sentence rather than falling. +##The next line is a setting you can change. +intonation 2 + +# Setting the pitch range. The first number gives a base pitch to the voice (value in Hertz). The second number controls the range of pitches used by the voice. +# Setting it equal to the first number will give a monotone sounding voice. The default values are 82 and 118. +pitch 69 96 + +## The tone setting. The first number on the setting line, 600, is the frequency setting for the amount of bass in the voice. +##The second number on the tone line is the volume of the bass frequency. You can set it from 0 to 255, 0 being the least amount, 255 being the most. +##The third number on the tone line, 1200, is the mid range frequency. The fourth number on the line is the setting to change the volume of the mid range frequency. +##0 being the least amount and 255 being the maximum. +## The fifth number on the tone line, 2000, is the treble frequency. The sixth number is the volume of the treble frequency. 0 is the minimum and 255 is the maximum. +## You will notice that all 3 frequencies are set to 255. +##The next line is a setting that you can change. +tone 1000 127 1200 127 2000 127 +##This file does not include all of the settings that can be used to modify an E Speak voice. It is intended to get you familiar with what the settings do. +##However, you can go to http://espeak.sourceforge.net/voices.html and read further information about other settings that can be added and changed. I hope this helps, and Have fun. diff --git a/tts/piper/espeak-ng-data/voices/!v/iven b/tts/piper/espeak-ng-data/voices/!v/iven new file mode 100644 index 0000000..3b8c120 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/iven @@ -0,0 +1,14 @@ +language variant +name Iven +pitch 74 118 +formant 0 52 133 88 +formant 1 87 82 76 +formant 2 94 56 42 +formant 3 93 52 130 +formant 4 110 76 65 +formant 5 102 45 20 +formant 6 40 50 50 +formant 7 60 50 60 +formant 8 100 50 40 +voicing 530 +tone 600 255 1200 255 2000 160 \ No newline at end of file diff --git a/tts/piper/espeak-ng-data/voices/!v/iven2 b/tts/piper/espeak-ng-data/voices/!v/iven2 new file mode 100644 index 0000000..e61fd73 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/iven2 @@ -0,0 +1,15 @@ +language variant +name Iven2 +pitch 74 118 +formant 0 52 133 88 +formant 1 87 82 76 +formant 2 94 56 42 +formant 3 93 52 130 +formant 4 110 76 65 +formant 5 102 45 20 +formant 6 40 50 50 +formant 7 60 50 60 +formant 8 100 50 40 +voicing 220 +consonants 28 42 +tone 600 255 1200 255 2000 150 \ No newline at end of file diff --git a/tts/piper/espeak-ng-data/voices/!v/iven3 b/tts/piper/espeak-ng-data/voices/!v/iven3 new file mode 100644 index 0000000..a58e745 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/iven3 @@ -0,0 +1,14 @@ +language variant +name Iven3 +pitch 74 118 +formant 0 52 133 88 +formant 1 87 82 76 +formant 2 94 56 42 +formant 3 93 52 130 +formant 4 110 76 65 +formant 5 102 45 20 +formant 6 40 50 50 +formant 7 60 50 60 +formant 8 100 50 40 +voicing 165 +tone 600 255 1200 255 2000 160 \ No newline at end of file diff --git a/tts/piper/espeak-ng-data/voices/!v/iven4 b/tts/piper/espeak-ng-data/voices/!v/iven4 new file mode 100644 index 0000000..43084c1 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/iven4 @@ -0,0 +1,14 @@ +language variant +name Iven4 +pitch 74 118 +formant 0 52 133 88 +formant 1 87 82 76 +formant 2 94 56 42 +formant 3 93 52 130 +formant 4 110 76 65 +formant 5 102 45 20 +formant 6 40 50 50 +formant 7 60 50 60 +formant 8 100 50 40 +voicing 165 +tone 600 170 1200 100 2000 40 \ No newline at end of file diff --git a/tts/piper/espeak-ng-data/voices/!v/john b/tts/piper/espeak-ng-data/voices/!v/john new file mode 100644 index 0000000..dce9446 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/john @@ -0,0 +1,50 @@ +##Please note the 2 number signs, or pound signs in this file are for comments to help you to understand what the settings are and how to set them. +## Language sets the language of your voice. This setting is required for every voice that you make. +##The next line is a setting you can change. However if you don't know the language codes it may be best to leave the setting as it is. +language variant + +## The name setting is the name that will show up in the voice settings in the variant combo box. +##The next line is a setting you can change +name John + +##The formant settings +## Formant 0 is used to give a low frequency component to the sounds. +## The three numbers are frequency, strength, and Width, in that order. Please note, the numbers are seperated by a space. +##The next line is a setting you can change +formant 0 100 100 100 + +# Formants 1,2, and 3 are the standard three formants which define vowels. +##The next 3 lines are settings you can change +formant 1 100 100 100 +formant 2 100 100 100 +formant 3 100 100 100 + +# Formants 4,5 are higher than F3. They affect the quality of the voice. +##The next 2 lines are settings that you can change. +formant 4 100 100 100 +formant 5 100 100 100 + +## Formants 6, 7, and 8 are weak, high frequency, additions to vowels to give a clearer sound. +##The next 3 lines are settings that you can change. +formant 6 100 100 100 +formant 7 100 100 100 +formant 8 100 100 100 + +##Intonation affects the rise and fall of the voice +## The settings are 1 default, 2 less intonation, 3 less intonation and commas do not raise the pitch, 4 the pitch rises at the end of a sentence rather than falling. +##The next line is a setting you can change. +intonation 1 + +# Setting the pitch range. The first number gives a base pitch to the voice (value in Hertz). The second number controls the range of pitches used by the voice. +# Setting it equal to the first number will give a monotone sounding voice. The default values are 82 and 118. +pitch 82 118 +## The tone setting. The first number on the setting line, 600, is the frequency setting for the amount of bass in the voice. +##The second number on the tone line is the volume of the bass frequency. You can set it from 0 to 255, 0 being the least amount, 255 being the most. +##The third number on the tone line, 1200, is the mid range frequency. The fourth number on the line is the setting to change the volume of the mid range frequency. +##0 being the least amount and 255 being the maximum. +## The fifth number on the tone line, 2000, is the treble frequency. The sixth number is the volume of the treble frequency. 0 is the minimum and 255 is the maximum. +## You will notice that all 3 frequencies are set to 255. +##The next line is a setting that you can change. +tone 600 255 1200 255 2000 255 +##This file does not include all of the settings that can be used to modify an E Speak voice. It is intended to get you familiar with what the settings do. +##However, you can go to http://espeak.sourceforge.net/voices.html and read further information about other settings that can be added and changed. I hope this helps, and Have fun. diff --git a/tts/piper/espeak-ng-data/voices/!v/kaukovalta b/tts/piper/espeak-ng-data/voices/!v/kaukovalta new file mode 100644 index 0000000..82cda33 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/kaukovalta @@ -0,0 +1,16 @@ +language variant +name Kaukovalta +formant 0 80 80 100 +formant 1 40 80 100 +formant 2 70 100 130 +formant 3 80 100 60 +formant 4 70 90 100 +formant 5 70 90 100 +formant 6 70 100 90 + formant 7 100 90 110 +formant 8 100 95 100 +pitch 70 120 +tone 100 130 800 130 2000 130 +consonants 70 70 +roughness 4 + \ No newline at end of file diff --git a/tts/piper/espeak-ng-data/voices/!v/klatt b/tts/piper/espeak-ng-data/voices/!v/klatt new file mode 100644 index 0000000..b739a86 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/klatt @@ -0,0 +1,4 @@ +language variant +name klatt +klatt 1 + diff --git a/tts/piper/espeak-ng-data/voices/!v/klatt2 b/tts/piper/espeak-ng-data/voices/!v/klatt2 new file mode 100644 index 0000000..01477be --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/klatt2 @@ -0,0 +1,4 @@ +language variant +name klatt2 +klatt 2 + diff --git a/tts/piper/espeak-ng-data/voices/!v/klatt3 b/tts/piper/espeak-ng-data/voices/!v/klatt3 new file mode 100644 index 0000000..b1a874b --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/klatt3 @@ -0,0 +1,4 @@ +language variant +name klatt3 +klatt 3 + diff --git a/tts/piper/espeak-ng-data/voices/!v/klatt4 b/tts/piper/espeak-ng-data/voices/!v/klatt4 new file mode 100644 index 0000000..6527808 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/klatt4 @@ -0,0 +1,4 @@ +language variant +name klatt4 +klatt 4 + diff --git a/tts/piper/espeak-ng-data/voices/!v/klatt5 b/tts/piper/espeak-ng-data/voices/!v/klatt5 new file mode 100644 index 0000000..9d831fb --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/klatt5 @@ -0,0 +1,4 @@ +language variant +name klatt5 +klatt 5 + diff --git a/tts/piper/espeak-ng-data/voices/!v/klatt6 b/tts/piper/espeak-ng-data/voices/!v/klatt6 new file mode 100644 index 0000000..7656ab3 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/klatt6 @@ -0,0 +1,4 @@ +language variant +name klatt6 +klatt 6 + diff --git a/tts/piper/espeak-ng-data/voices/!v/linda b/tts/piper/espeak-ng-data/voices/!v/linda new file mode 100644 index 0000000..a56a8b0 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/linda @@ -0,0 +1,20 @@ +language variant +name Linda +gender female + +#echo 130 15 +pitch 200 247 +flutter 3 +formant 0 88 85 154 +formant 1 135 58 169 -30 +formant 2 131 75 152 -260 +formant 3 123 75 150 +formant 4 125 80 150 +formant 5 125 80 150 +formant 6 110 80 150 +formant 7 110 75 150 +formant 8 110 75 150 + +stressAdd -20 -20 -20 -20 0 3 20 120 +stressAmp 18 16 20 20 20 20 27 20 + diff --git a/tts/piper/espeak-ng-data/voices/!v/m1 b/tts/piper/espeak-ng-data/voices/!v/m1 new file mode 100644 index 0000000..4cc9a00 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/m1 @@ -0,0 +1,20 @@ +language variant +name male1 +gender male 70 + +pitch 75 109 +flutter 5 +roughness 4 +consonants 80 100 + +formant 0 98 100 100 +formant 1 97 100 100 +formant 2 97 95 100 +formant 3 97 95 100 +formant 4 97 85 100 +formant 5 105 80 100 +formant 6 95 80 100 +formant 7 100 100 100 +formant 8 100 100 100 + +//stressAdd -10 -10 -20 -20 0 0 40 70 diff --git a/tts/piper/espeak-ng-data/voices/!v/m2 b/tts/piper/espeak-ng-data/voices/!v/m2 new file mode 100644 index 0000000..c234f46 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/m2 @@ -0,0 +1,15 @@ +language variant +name male2 +gender male + +pitch 88 115 +echo 130 15 +formant 0 100 80 120 +formant 1 90 85 120 +formant 2 110 85 120 +formant 3 105 90 120 +formant 4 100 90 120 +formant 5 100 90 120 +formant 6 100 90 120 +formant 7 100 90 120 +formant 8 100 90 120 diff --git a/tts/piper/espeak-ng-data/voices/!v/m3 b/tts/piper/espeak-ng-data/voices/!v/m3 new file mode 100644 index 0000000..00479dc --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/m3 @@ -0,0 +1,17 @@ +language variant +name male3 +gender male + +pitch 80 122 +formant 0 100 100 100 +formant 1 96 97 100 +formant 2 96 97 100 +formant 3 96 103 100 +formant 4 95 103 100 +formant 5 95 103 100 +formant 6 100 100 100 +formant 7 100 100 100 +formant 8 100 100 100 + +consonants 100 +stressAdd 10 10 0 0 0 0 -30 -30 diff --git a/tts/piper/espeak-ng-data/voices/!v/m4 b/tts/piper/espeak-ng-data/voices/!v/m4 new file mode 100644 index 0000000..7199341 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/m4 @@ -0,0 +1,17 @@ +language variant +name male4 +gender male + +pitch 70 110 + +formant 0 103 100 100 +formant 1 103 100 100 +formant 2 103 100 100 +formant 3 103 100 100 +formant 4 106 100 100 +formant 5 106 100 100 +formant 6 106 100 100 +formant 7 103 100 100 +formant 8 103 100 100 + +stressAdd -10 -10 -30 -30 0 0 60 90 diff --git a/tts/piper/espeak-ng-data/voices/!v/m5 b/tts/piper/espeak-ng-data/voices/!v/m5 new file mode 100644 index 0000000..d258656 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/m5 @@ -0,0 +1,15 @@ +language variant +name male5 +gender male + +formant 0 100 85 130 +formant 1 90 85 130 40 +formant 2 80 85 130 310 +formant 3 105 85 130 +formant 4 105 85 130 +formant 5 105 85 130 +formant 6 105 85 150 +formant 7 105 85 150 +formant 8 105 85 150 + +intonation 2 diff --git a/tts/piper/espeak-ng-data/voices/!v/m6 b/tts/piper/espeak-ng-data/voices/!v/m6 new file mode 100644 index 0000000..bd336a9 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/m6 @@ -0,0 +1,13 @@ +language variant +name male6 +gender male + +pitch 82 117 + +formant 0 100 90 120 +formant 1 100 90 140 +formant 2 100 70 140 +formant 3 100 75 140 +formant 4 100 80 140 +formant 5 100 80 140 + diff --git a/tts/piper/espeak-ng-data/voices/!v/m7 b/tts/piper/espeak-ng-data/voices/!v/m7 new file mode 100644 index 0000000..11b49ed --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/m7 @@ -0,0 +1,17 @@ +language variant +name male7 +gender male + +pitch 75 125 + +formant 0 100 125 100 +formant 1 100 90 80 +formant 2 100 70 90 +formant 3 100 60 90 +formant 4 100 60 90 +formant 5 75 50 90 +formant 6 90 50 100 +formant 7 100 50 100 +formant 8 100 50 100 +voicing 155 + diff --git a/tts/piper/espeak-ng-data/voices/!v/m8 b/tts/piper/espeak-ng-data/voices/!v/m8 new file mode 100644 index 0000000..c03ca3e --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/m8 @@ -0,0 +1,16 @@ +language variant +name male8 +gender male 50 + +pitch 65 102 +formant 0 100 125 100 +formant 1 96 90 80 +formant 2 97 70 90 +formant 3 97 60 90 +formant 4 97 60 90 +formant 5 100 50 90 +formant 6 90 50 100 +formant 7 100 50 100 +formant 8 100 50 100 + +tone 100 255 600 70 1200 22 2000 66 3000 12 diff --git a/tts/piper/espeak-ng-data/voices/!v/marcelo b/tts/piper/espeak-ng-data/voices/!v/marcelo new file mode 100644 index 0000000..8df9651 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/marcelo @@ -0,0 +1,17 @@ +language variant +name Marcelo + +pitch 65 115 + +formant 0 65 161 35 +formant 1 75 131 65 +formant 2 90 60 40 +formant 3 59 50 55 +formant 4 69 65 35 +formant 5 69 60 25 +formant 6 59 60 35 +formant 7 149 0 10 +formant 8 199 0 90 +voicing 135 +consonants 115 120 + diff --git a/tts/piper/espeak-ng-data/voices/!v/max b/tts/piper/espeak-ng-data/voices/!v/max new file mode 100644 index 0000000..e3c2889 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/max @@ -0,0 +1,15 @@ +language variant +name Max + +formant 0 90 120 100 +formant 1 100 100 75 +formant 2 100 100 75 +formant 3 100 80 75 +formant 4 100 80 75 +formant 5 100 80 75 +formant 6 100 0 75 +formant 7 100 0 75 +formant 8 100 0 75 + + + diff --git a/tts/piper/espeak-ng-data/voices/!v/michel b/tts/piper/espeak-ng-data/voices/!v/michel new file mode 100644 index 0000000..b9b5ecb --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/michel @@ -0,0 +1,22 @@ +language variant +name Michel +gender male 25 +pitch 82 122 +echo 0 0 +flutter 0 +roughness 0 +stressAmp 20 18 20 20 20 22 22 22 + + +formant 0 105 200 140 +formant 1 95 150 120 +formant 2 100 120 140 +formant 3 95 95 140 +formant 4 30 30 30 -100 +formant 5 90 90 90 +formant 6 110 60 65 +formant 7 100 0 100 +formant 8 100 0 100 +voicing 35 +consonants 60 40 +tone 400 160 1500 100 3000 70 4500 40 diff --git a/tts/piper/espeak-ng-data/voices/!v/miguel b/tts/piper/espeak-ng-data/voices/!v/miguel new file mode 100644 index 0000000..53d71e7 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/miguel @@ -0,0 +1,22 @@ +language variant +name Miguel +gender male 25 +pitch 80 130 +echo 0 0 +flutter 0 +roughness 0 +stressAmp 20 18 20 20 20 22 22 22 + + +formant 0 105 200 140 +formant 1 95 150 120 +formant 2 100 120 140 +formant 3 95 95 140 +formant 4 30 30 30 -100 +formant 5 90 90 90 +formant 6 110 60 65 +formant 7 100 0 100 +formant 8 100 0 100 +voicing 35 +consonants 60 40 +tone 300 240 400 160 1500 100 3000 70 diff --git a/tts/piper/espeak-ng-data/voices/!v/mike2 b/tts/piper/espeak-ng-data/voices/!v/mike2 new file mode 100644 index 0000000..2715f4d --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/mike2 @@ -0,0 +1,12 @@ +language variant +name Mike2 +klatt 6 +voicing 170 +pitch 67 107 +formant 1 95 100 100 +formant 2 95 100 100 +formant 3 105 100 100 +formant 4 115 100 100 +formant 5 115 100 100 + +consonants 70 150 diff --git a/tts/piper/espeak-ng-data/voices/!v/norbert b/tts/piper/espeak-ng-data/voices/!v/norbert new file mode 100644 index 0000000..a210789 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/norbert @@ -0,0 +1,50 @@ +##Please note the 2 number signs, or pound signs in this file are for comments to help you to understand what the settings are and how to set them. +## Language sets the language of your voice. This setting is required for every voice that you make. +##The next line is a setting you can change. However if you don't know the language codes it may be best to leave the setting as it is. +language variant + +## The name setting is the name that will show up in the voice settings in the variant combo box. +##The next line is a setting you can change +name norbert + +##The formant settings +## Formant 0 is used to give a low frequency component to the sounds. +## The three numbers are frequency, strength, and Width, in that order. Please note, the numbers are seperated by a space. +##The next line is a setting you can change +formant 0 100 100 100 + +# Formants 1,2, and 3 are the standard three formants which define vowels. +##The next 3 lines are settings you can change +formant 1 100 100 100 +formant 2 75 50 100 +formant 3 100 100 100 + +# Formants 4,5 are higher than F3. They affect the quality of the voice. +##The next 2 lines are settings that you can change. +formant 4 100 100 100 +formant 5 100 100 100 + +## Formants 6, 7, and 8 are weak, high frequency, additions to vowels to give a clearer sound. +##The next 3 lines are settings that you can change. +formant 6 100 100 100 +formant 7 100 100 100 +formant 8 100 100 100 + +##Intonation affects the rise and fall of the voice +## The settings are 1 default, 2 less intonation, 3 less intonation and commas do not raise the pitch, 4 the pitch rises at the end of a sentence rather than falling. +##The next line is a setting you can change. +intonation 1 + +# Setting the pitch range. The first number gives a base pitch to the voice (value in Hertz). The second number controls the range of pitches used by the voice. Setting it equal +# to the first number will give a monotone sounding voice. The default values are 82 and 118. +pitch 82 118 +## The tone setting. The first number on the setting line, 600, is the frequency setting for the amount of bass in the voice. +## The second number on the tone line is the volume of the bass frequency. You can set it from 0 to 255, 0 being the least amount, 255 being the most. +##The third number on the tone line, 1200, is the mid range frequency. The fourth number on the line is the setting to change the volume of the mid range frequency. +##0 being the least amount and 255 being the maximum. +## The fifth number on the tone line, 2000, is the treble frequency. The sixth number is the volume of the treble frequency. 0 is the minimum and 255 is the maximum. +## You will notice that all 3 frequencies are set to 255. +##The next line is a setting that you can change. +tone 600 255 1000 100 5000 255 +##This file does not include all of the settings that can be used to modify an E Speak voice. It is intended to get you familiar with what the settings do. +##However, you can go to http://espeak.sourceforge.net/voices.html and read further information about other settings that can be added and changed. I hope this helps, and Have fun. diff --git a/tts/piper/espeak-ng-data/voices/!v/pablo b/tts/piper/espeak-ng-data/voices/!v/pablo new file mode 100644 index 0000000..031e5fe --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/pablo @@ -0,0 +1,52 @@ +##Pleas note the 2 number signs, or pound signs in this file are for comments to help you to understand what the settings are and how to set them. +## Language sets the language of your voice. This setting is required for every voice that you make. +##The next line is a setting you can change. However if you don't know the language codes it may be best to leave the setting as it is. +language variant + +## The name setting is the name that will show up in the voice settings in the variant combo box. +##The next line is a setting you can change +name Pablo + +##The formant settings +## Formant 0 is used to give a low frequency component to the sounds. +## The three numbers are frequency, strength, and Width, in that order. Please note, the numbers are seperated by a space. +##The next line is a setting you can change +formant 0 90 100 90 + +# Formants 1,2, and 3 are the standard three formants which define vowels. +##The next 3 lines are settings you can change +formant 1 95 100 80 +formant 2 97 100 80 +formant 3 98 90 80 + +# Formants 4,5 are higher than F3. They affect the quality of the voice. +##The next 2 lines are settings that you can change. +formant 4 110 100 100 +formant 5 110 100 100 + +## Formants 6, 7, and 8 are weak, high frequency, additions to vowels to give a clearer sound. +##The next 3 lines are settings that you can change. +formant 6 100 100 100 +formant 7 100 100 100 +formant 8 100 100 100 + +##Intonation affects the rise and fall of the voice +## The settings are 1 default, 2 less intonation, 3 less intonation and commas do not raise the pitch, 4 the pitch rises at the end of a sentence rather than falling. +##The next line is a setting you can change. +intonation 3 +echo 30 30 +# Setting the pitch range. The first number gives a base pitch to the voice (value in Hertz). The second number controls the range of pitches used by the voice. Setting it equal +# to the first number will give a monotone sounding voice. The default values are 82 and 118. +pitch 82 130 +## The tone setting. The first number on the setting line, 600, is the frequency setting for the amount of bass in the voice. +## The second number on the tone line is the volume of the bass frequency. You can set it from 0 to 255, 0 being the least amount, 255 being the most. +##The third number on the tone line, 1200, is the mid range frequency. The fourth number on the line is the setting to change the volume of the mid range frequency. +## 0 being the least amount and 255 being the maximum. +## The fifth number on the tone line, 2000, is the treble frequency. The sixth number is the volume of the treble frequency. 0 is the minimum and 255 is the maximum. +## You will notice that all 3 frequencies are set to 255. +##The next line is a setting that you can change. +tone 600 255 1200 200 2000 255 + +##This file does not include all of the settings that can be used to modify an E Speak voice. It is intended to get you familiar with what the settings do. +#However, you can go to http://espeak.sourceforge.net/voices.html and read further information about other settings that can be added and changed. I hope this helps, and Have fun. + diff --git a/tts/piper/espeak-ng-data/voices/!v/paul b/tts/piper/espeak-ng-data/voices/!v/paul new file mode 100644 index 0000000..9015cd3 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/paul @@ -0,0 +1,17 @@ +language variant +name Paul + +pitch 70 100 + +formant 0 90 120 100 +formant 1 103 100 75 +formant 2 98 100 75 +formant 3 100 80 75 +formant 4 102 30 100 +formant 5 100 80 100 +formant 6 100 80 75 +formant 7 100 0 75 +formant 8 100 60 75 +consonants 90 60 +voicing 230 +tone 420 255 1300 130 4000 100 \ No newline at end of file diff --git a/tts/piper/espeak-ng-data/voices/!v/pedro b/tts/piper/espeak-ng-data/voices/!v/pedro new file mode 100644 index 0000000..5cc34db --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/pedro @@ -0,0 +1,23 @@ +language variant + +name Pedro + +formant 0 100 150 100 + +formant 1 95 100 80 +formant 2 95 100 80 +formant 3 100 100 90 + +formant 4 100 100 100 +formant 5 100 100 100 + +formant 6 100 100 100 +formant 7 100 100 100 +formant 8 100 100 100 + +intonation 3 + +pitch 82 118 +tone 600 255 1200 255 2000 255 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 130 140 140 100 0 0 130 160 diff --git a/tts/piper/espeak-ng-data/voices/!v/quincy b/tts/piper/espeak-ng-data/voices/!v/quincy new file mode 100644 index 0000000..dd75dad --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/quincy @@ -0,0 +1,20 @@ +language variant +name Quincy + +pitch 67 100 + +formant 0 85 108 106 3 +formant 1 97 110 56 +formant 2 96 80 60 +formant 3 101 50 50 +formant 4 110 33 55 +formant 5 110 22 65 +formant 6 77 60 60 65 +formant 7 66 0 100 +formant 8 100 0 100 +voicing 99 +consonants 66 90 + +roughness 0 +tone 600 170 1200 100 2000 70 +stressAmp 16 16 24 20 20 16 28 24 diff --git a/tts/piper/espeak-ng-data/voices/!v/rob b/tts/piper/espeak-ng-data/voices/!v/rob new file mode 100644 index 0000000..d7c7ae4 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/rob @@ -0,0 +1,17 @@ +language variant +name Rob + +pitch 50 130 +formant 0 100 100 100 +formant 1 95 100 60 +formant 2 97 90 50 +formant 3 101 70 50 +formant 4 110 65 55 +formant 5 110 70 65 +formant 6 110 70 65 +formant 7 0 0 0 +formant 8 0 0 0 + +voicing 115 +consonants 110 120 + diff --git a/tts/piper/espeak-ng-data/voices/!v/robert b/tts/piper/espeak-ng-data/voices/!v/robert new file mode 100644 index 0000000..46f5075 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/robert @@ -0,0 +1,17 @@ +language variant +name Robert + +pitch 65 115 + +formant 0 85 108 100 +formant 1 95 110 60 +formant 2 97 90 50 +formant 3 101 50 50 +formant 4 110 65 55 +formant 5 110 60 65 +formant 6 110 60 65 +formant 7 100 0 100 +formant 8 100 0 100 +voicing 115 +consonants 110 120 + diff --git a/tts/piper/espeak-ng-data/voices/!v/robosoft b/tts/piper/espeak-ng-data/voices/!v/robosoft new file mode 100644 index 0000000..0668d9a --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/robosoft @@ -0,0 +1,26 @@ +language variant +name Robosoft +echo 30 1000 +klatt 5 + +pitch 60 90 +formant 0 100 125 100 +formant 1 96 90 80 +formant 2 97 70 90 +formant 3 97 60 90 +formant 4 97 60 90 +formant 5 100 50 90 +formant 6 90 50 100 +formant 7 100 50 100 +formant 8 100 50 100 + +roughness 50 + +intonation 0 +voicing 80 +consonants 110 120 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 130 140 140 100 0 0 130 160 +stressAmp 16 16 24 24 16 16 20 24 + +tone 100 255 600 70 1200 22 2000 66 3000 12 diff --git a/tts/piper/espeak-ng-data/voices/!v/robosoft2 b/tts/piper/espeak-ng-data/voices/!v/robosoft2 new file mode 100644 index 0000000..1695414 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/robosoft2 @@ -0,0 +1,26 @@ +language variant +name Robosoft2 +echo 10 600 +klatt 4 + +pitch 70 100 +formant 0 100 125 100 +formant 1 96 90 80 +formant 2 97 70 90 +formant 3 97 60 90 +formant 4 97 60 90 +formant 5 100 50 90 +formant 6 90 50 100 +formant 7 100 50 100 +formant 8 100 50 100 + +roughness 75 + +intonation -25 +voicing 80 +consonants 110 120 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 130 140 140 100 0 0 130 160 +stressAmp 16 16 24 24 16 16 20 24 + +tone 100 255 600 70 1200 22 2000 66 3000 12 diff --git a/tts/piper/espeak-ng-data/voices/!v/robosoft3 b/tts/piper/espeak-ng-data/voices/!v/robosoft3 new file mode 100644 index 0000000..15bbe34 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/robosoft3 @@ -0,0 +1,26 @@ +language variant +name Robosoft3 +echo 10 10000 +klatt 4 + +pitch 75 115 +formant 0 100 125 100 +formant 1 96 90 80 +formant 2 97 70 90 +formant 3 97 60 90 +formant 4 97 60 90 +formant 5 100 50 90 +formant 6 90 50 100 +formant 7 100 50 100 +formant 8 100 50 100 + +roughness 5 + +intonation 10 +voicing 150 +consonants 110 120 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 130 140 140 100 0 0 130 160 +stressAmp 16 16 24 24 16 16 20 24 + +tone 100 255 600 70 1200 22 2000 66 3000 12 diff --git a/tts/piper/espeak-ng-data/voices/!v/robosoft4 b/tts/piper/espeak-ng-data/voices/!v/robosoft4 new file mode 100644 index 0000000..5161a4f --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/robosoft4 @@ -0,0 +1,25 @@ +language variant +name Robosoft4 +echo 10 10000 + +pitch 75 115 +formant 0 100 125 100 +formant 1 96 90 80 +formant 2 97 70 90 +formant 3 97 60 90 +formant 4 97 60 90 +formant 5 100 50 90 +formant 6 90 50 100 +formant 7 100 50 100 +formant 8 100 50 100 + +roughness 5 + +intonation 10 +voicing 150 +consonants 110 120 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 130 140 140 100 0 0 130 160 +stressAmp 16 16 24 24 16 16 20 24 + +tone 100 255 600 70 1200 22 2000 66 3000 12 diff --git a/tts/piper/espeak-ng-data/voices/!v/robosoft5 b/tts/piper/espeak-ng-data/voices/!v/robosoft5 new file mode 100644 index 0000000..2420f89 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/robosoft5 @@ -0,0 +1,25 @@ +language variant +name Robosoft5 +echo 10 10000 + +pitch 75 115 +formant 0 100 125 100 +formant 1 96 90 80 +formant 2 97 70 90 +formant 3 97 60 90 +formant 4 97 60 90 +formant 5 100 50 90 +formant 6 90 50 100 +formant 7 100 50 100 +formant 8 100 50 100 + +roughness 0 + +intonation 10 +voicing 150 +consonants 60 40 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 130 140 140 100 0 0 130 160 +stressAmp 16 16 24 24 16 16 20 24 + +tone 100 255 600 70 1200 22 2000 66 3000 12 diff --git a/tts/piper/espeak-ng-data/voices/!v/robosoft6 b/tts/piper/espeak-ng-data/voices/!v/robosoft6 new file mode 100644 index 0000000..d3be158 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/robosoft6 @@ -0,0 +1,15 @@ +language variant +name Robosoft6 +echo 40 10000 +pitch 150 150 +formant 0 100 125 100 +formant 1 96 90 80 +formant 2 97 70 90 +formant 3 97 60 90 +formant 4 97 60 90 +formant 5 100 50 90 +formant 6 90 50 100 +formant 7 100 50 100 +formant 8 100 50 100 + +tone 100 255 600 70 1200 22 2000 66 3000 12 diff --git a/tts/piper/espeak-ng-data/voices/!v/robosoft7 b/tts/piper/espeak-ng-data/voices/!v/robosoft7 new file mode 100644 index 0000000..25bf759 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/robosoft7 @@ -0,0 +1,25 @@ +language variant +name Robosoft7 +echo 10 10000 + +pitch 75 115 + +formant 0 90 120 100 +formant 1 100 100 75 +formant 2 100 100 75 +formant 3 100 80 75 +formant 4 100 80 75 +formant 5 100 80 75 +formant 6 100 0 75 +formant 7 100 0 75 +formant 8 100 0 75 + +roughness 0 + +intonation 10 +voicing 150 +consonants 60 40 +stressLength 0 1 2 3 4 5 6 7 +stressAdd 130 140 140 100 0 0 130 160 + +tone 100 255 600 70 1200 22 2000 66 3000 12 diff --git a/tts/piper/espeak-ng-data/voices/!v/robosoft8 b/tts/piper/espeak-ng-data/voices/!v/robosoft8 new file mode 100644 index 0000000..5c84bd5 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/robosoft8 @@ -0,0 +1,16 @@ +language variant +name Robosoft8 +echo 40 10000 +pitch 150 150 +formant 0 90 120 100 +formant 1 100 100 75 +formant 2 100 100 75 +formant 3 100 80 75 +formant 4 100 80 75 +formant 5 100 80 75 +formant 6 100 0 75 +formant 7 100 0 75 +formant 8 100 0 75 + + + diff --git a/tts/piper/espeak-ng-data/voices/!v/sandro b/tts/piper/espeak-ng-data/voices/!v/sandro new file mode 100644 index 0000000..6dcfe02 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/sandro @@ -0,0 +1,25 @@ +// This file is UTF-8 encoded +// Variant sandro (ver.25-3) for eSpeak-ng Copyright (C)2019 by Lolo vmanolo301@gmail.com + +language variant +name sandro +gender male + +formant 0 95 146 100 +formant 1 98 90 100 +formant 2 103 98 100 +formant 3 100 90 100 +formant 4 100 101 100 +formant 5 110 120 100 2123 +formant 6 100 100 100 1200 +formant 7 32 125 80 600 +formant 8 34 95 30 49 + +voicing 165 +consonants 194 255 +pitch 78 115 +roughness 3 +breath 20 5 2 10 5 0 27 100 +breathw 255 255 60 180 160 255 255 255 + +tone 500 210 470 70 160 155 2985 32 diff --git a/tts/piper/espeak-ng-data/voices/!v/shelby b/tts/piper/espeak-ng-data/voices/!v/shelby new file mode 100644 index 0000000..21292d4 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/shelby @@ -0,0 +1,18 @@ +language variant +name shelby +flutter 0 +roughness 0 + +formant 0 100 160 190 +formant 1 90 90 90 +formant 2 140 140 140 +formant 3 130 150 130 +formant 4 110 110 110 +formant 5 120 120 110 +formant 6 10 10 10 +formant 7 10 10 10 +formant 8 10 10 10 + +pitch 100 210 +voicing 40 +consonants 90 70 \ No newline at end of file diff --git a/tts/piper/espeak-ng-data/voices/!v/steph b/tts/piper/espeak-ng-data/voices/!v/steph new file mode 100644 index 0000000..e60b0dc --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/steph @@ -0,0 +1,21 @@ +language variant +name Steph +gender female + +pitch 166 200 +flutter 1 +roughness 0 +tone 100 255 600 70 1200 22 2000 66 3000 12 + +formant 0 99 80 150 +formant 1 120 60 160 +formant 2 99 70 110 150 +formant 3 116 77 150 +formant 4 9 59 110 +formant 5 100 50 2 +formant 6 104 80 150 +formant 7 110 70 150 +formant 8 110 70 150 + +stressAmp 16 16 24 24 16 16 20 24 +consonants 55 90 diff --git a/tts/piper/espeak-ng-data/voices/!v/steph2 b/tts/piper/espeak-ng-data/voices/!v/steph2 new file mode 100644 index 0000000..6ac109b --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/steph2 @@ -0,0 +1,21 @@ +language variant +name Steph2 +gender female + +pitch 166 200 +flutter 1 +roughness 0 +tone 100 255 600 70 1200 22 2000 66 3000 12 + +formant 0 99 100 150 +formant 1 120 80 160 +formant 2 99 90 110 150 +formant 3 116 97 150 +formant 4 9 73 116 +formant 5 100 70 2 +formant 6 104 100 150 +formant 7 110 90 150 +formant 8 110 90 150 + +stressAmp 16 16 24 24 16 16 20 24 +consonants 55 90 diff --git a/tts/piper/espeak-ng-data/voices/!v/steph3 b/tts/piper/espeak-ng-data/voices/!v/steph3 new file mode 100644 index 0000000..a925755 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/steph3 @@ -0,0 +1,22 @@ +language variant +name Steph3 +gender female + +pitch 166 200 +flutter 1 +roughness 0 +voicing 200 +tone 100 255 600 70 1200 22 2000 66 3000 12 + +formant 0 99 80 150 +formant 1 120 60 160 +formant 2 99 70 110 150 +formant 3 116 77 150 +formant 4 9 59 110 +formant 5 100 50 2 +formant 6 104 80 150 +formant 7 110 70 150 +formant 8 110 70 150 + +stressAmp 16 16 24 24 16 16 20 24 +consonants 70 90 diff --git a/tts/piper/espeak-ng-data/voices/!v/travis b/tts/piper/espeak-ng-data/voices/!v/travis new file mode 100644 index 0000000..56c22f9 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/travis @@ -0,0 +1,23 @@ +language variant +name travis +gender male 30 + +pitch 75 120 + +formant 0 90 90 90 90 +formant 1 50 100 80 95 +formant 2 90 60 90 100 +formant 3 80 80 90 100 +formant 4 50 90 100 100 +formant 5 100 95 100 55 +formant 6 80 50 100 85 +formant 7 60 60 60 120 +formant 8 80 80 140 100 + +tone 600 100 1000 200 1500 50 +flutter 1 + +roughness 3 + +voicing 200 +consonants 120 190 diff --git a/tts/piper/espeak-ng-data/voices/!v/victor b/tts/piper/espeak-ng-data/voices/!v/victor new file mode 100644 index 0000000..fa275e8 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/victor @@ -0,0 +1,16 @@ +language variant +name victor +gender male 25 + +formant 0 100 100 100 +formant 1 95 95 95 +formant 2 90 90 90 +formant 3 90 90 90 +formant 4 40 40 40 +formant 5 80 80 80 +formant 6 20 20 20 +formant 7 20 20 20 +formant 8 20 20 20 +pitch 80 110 +voicing 60 +breath 2 4 \ No newline at end of file diff --git a/tts/piper/espeak-ng-data/voices/!v/whisper b/tts/piper/espeak-ng-data/voices/!v/whisper new file mode 100644 index 0000000..4f8f5e8 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/whisper @@ -0,0 +1,13 @@ +language variant +name whisper +gender male + +pitch 82 117 +flutter 20 + +formant 0 100 0 100 +formant 1 100 80 100 + +voicing 17 +breath 75 75 50 40 15 10 +breathw 150 150 200 200 400 400 diff --git a/tts/piper/espeak-ng-data/voices/!v/whisperf b/tts/piper/espeak-ng-data/voices/!v/whisperf new file mode 100644 index 0000000..f239e8a --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/whisperf @@ -0,0 +1,24 @@ +language variant +name female_whisper +gender female + +pitch 160 220 +roughness 3 + +formant 0 105 0 150 +formant 1 110 40 160 +formant 2 110 70 150 +formant 3 110 70 150 +formant 4 115 80 150 +formant 5 115 80 150 +formant 6 110 70 150 +formant 7 110 70 150 +formant 8 110 70 150 + +stressAdd 0 0 -10 -10 0 0 10 40 + +// whisper +voicing 20 +breath 75 75 50 40 15 10 +breathw 150 150 200 200 400 400 + diff --git a/tts/piper/espeak-ng-data/voices/!v/zac b/tts/piper/espeak-ng-data/voices/!v/zac new file mode 100644 index 0000000..ca415a6 --- /dev/null +++ b/tts/piper/espeak-ng-data/voices/!v/zac @@ -0,0 +1,15 @@ +language variant +name Zac +flutter 5 + +pitch 240 390 +formant 0 145 100 145 +formant 1 145 100 145 +formant 2 145 100 145 +formant 3 145 100 145 +formant 4 145 100 145 +formant 5 145 120 145 +formant 6 145 120 145 +formant 7 145 120 145 +formant 8 145 120 145 +voicing 80 diff --git a/tts/piper/espeak-ng-data/yue_dict b/tts/piper/espeak-ng-data/yue_dict new file mode 100644 index 0000000..ed9cf07 Binary files /dev/null and b/tts/piper/espeak-ng-data/yue_dict differ diff --git a/tts/piper/libespeak-ng.so b/tts/piper/libespeak-ng.so new file mode 120000 index 0000000..ab4142e --- /dev/null +++ b/tts/piper/libespeak-ng.so @@ -0,0 +1 @@ +libespeak-ng.so.1 \ No newline at end of file diff --git a/tts/piper/libespeak-ng.so.1 b/tts/piper/libespeak-ng.so.1 new file mode 120000 index 0000000..0925e09 --- /dev/null +++ b/tts/piper/libespeak-ng.so.1 @@ -0,0 +1 @@ +libespeak-ng.so.1.52.0.1 \ No newline at end of file diff --git a/tts/piper/libespeak-ng.so.1.52.0.1 b/tts/piper/libespeak-ng.so.1.52.0.1 new file mode 100644 index 0000000..1341ee5 Binary files /dev/null and b/tts/piper/libespeak-ng.so.1.52.0.1 differ diff --git a/tts/piper/libonnxruntime.so b/tts/piper/libonnxruntime.so new file mode 120000 index 0000000..b5f2af1 --- /dev/null +++ b/tts/piper/libonnxruntime.so @@ -0,0 +1 @@ +libonnxruntime.so.1.14.1 \ No newline at end of file diff --git a/tts/piper/libonnxruntime.so.1.14.1 b/tts/piper/libonnxruntime.so.1.14.1 new file mode 100644 index 0000000..eb35d62 Binary files /dev/null and b/tts/piper/libonnxruntime.so.1.14.1 differ diff --git a/tts/piper/libpiper_phonemize.so b/tts/piper/libpiper_phonemize.so new file mode 120000 index 0000000..e6b4b0d --- /dev/null +++ b/tts/piper/libpiper_phonemize.so @@ -0,0 +1 @@ +libpiper_phonemize.so.1 \ No newline at end of file diff --git a/tts/piper/libpiper_phonemize.so.1 b/tts/piper/libpiper_phonemize.so.1 new file mode 120000 index 0000000..c3e0382 --- /dev/null +++ b/tts/piper/libpiper_phonemize.so.1 @@ -0,0 +1 @@ +libpiper_phonemize.so.1.2.0 \ No newline at end of file diff --git a/tts/piper/libpiper_phonemize.so.1.2.0 b/tts/piper/libpiper_phonemize.so.1.2.0 new file mode 100644 index 0000000..b9788a0 Binary files /dev/null and b/tts/piper/libpiper_phonemize.so.1.2.0 differ diff --git a/tts/piper/libtashkeel_model.ort b/tts/piper/libtashkeel_model.ort new file mode 100644 index 0000000..7bd74eb Binary files /dev/null and b/tts/piper/libtashkeel_model.ort differ diff --git a/tts/piper/piper b/tts/piper/piper new file mode 100755 index 0000000..8b655a3 Binary files /dev/null and b/tts/piper/piper differ diff --git a/tts/piper/piper_phonemize b/tts/piper/piper_phonemize new file mode 100755 index 0000000..c7ae6df Binary files /dev/null and b/tts/piper/piper_phonemize differ diff --git a/tts/test_es.wav b/tts/test_es.wav new file mode 100644 index 0000000..7e49bf9 Binary files /dev/null and b/tts/test_es.wav differ diff --git a/tts/voices/de_DE-thorsten-medium.onnx b/tts/voices/de_DE-thorsten-medium.onnx new file mode 100644 index 0000000..8f655e1 Binary files /dev/null and b/tts/voices/de_DE-thorsten-medium.onnx differ diff --git a/tts/voices/de_DE-thorsten-medium.onnx.json b/tts/voices/de_DE-thorsten-medium.onnx.json new file mode 100644 index 0000000..71e1158 --- /dev/null +++ b/tts/voices/de_DE-thorsten-medium.onnx.json @@ -0,0 +1,487 @@ +{ + "audio": { + "sample_rate": 22050, + "quality": "medium" + }, + "espeak": { + "voice": "de" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + "_": [ + 0 + ], + "^": [ + 1 + ], + "$": [ + 2 + ], + " ": [ + 3 + ], + "!": [ + 4 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "β": [ + 125 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "ⱱ": [ + 129 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + "̧": [ + 140 + ], + "̃": [ + 141 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̩": [ + 144 + ], + "ʰ": [ + 145 + ], + "ˤ": [ + 146 + ], + "ε": [ + 147 + ], + "↓": [ + 148 + ], + "#": [ + 149 + ], + "\"": [ + 150 + ], + "↑": [ + 151 + ] + }, + "num_symbols": 256, + "num_speakers": 1, + "speaker_id_map": {}, + "piper_version": "1.0.0", + "language": { + "code": "de_DE", + "family": "de", + "region": "DE", + "name_native": "Deutsch", + "name_english": "German", + "country_english": "Germany" + }, + "dataset": "thorsten" +} \ No newline at end of file diff --git a/tts/voices/en_US-amy-medium.onnx b/tts/voices/en_US-amy-medium.onnx new file mode 100644 index 0000000..ef4cc05 Binary files /dev/null and b/tts/voices/en_US-amy-medium.onnx differ diff --git a/tts/voices/en_US-amy-medium.onnx.json b/tts/voices/en_US-amy-medium.onnx.json new file mode 100644 index 0000000..5a1e0a2 --- /dev/null +++ b/tts/voices/en_US-amy-medium.onnx.json @@ -0,0 +1,493 @@ +{ + "audio": { + "sample_rate": 22050, + "quality": "medium" + }, + "espeak": { + "voice": "en-us" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + "_": [ + 0 + ], + "^": [ + 1 + ], + "$": [ + 2 + ], + " ": [ + 3 + ], + "!": [ + 4 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "β": [ + 125 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "ⱱ": [ + 129 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + "̧": [ + 140 + ], + "̃": [ + 141 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̩": [ + 144 + ], + "ʰ": [ + 145 + ], + "ˤ": [ + 146 + ], + "ε": [ + 147 + ], + "↓": [ + 148 + ], + "#": [ + 149 + ], + "\"": [ + 150 + ], + "↑": [ + 151 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ] + }, + "num_symbols": 256, + "num_speakers": 1, + "speaker_id_map": {}, + "piper_version": "1.0.0", + "language": { + "code": "en_US", + "family": "en", + "region": "US", + "name_native": "English", + "name_english": "English", + "country_english": "United States" + }, + "dataset": "amy" +} \ No newline at end of file diff --git a/tts/voices/es_ES-davefx-medium.onnx b/tts/voices/es_ES-davefx-medium.onnx new file mode 100644 index 0000000..7b7546b Binary files /dev/null and b/tts/voices/es_ES-davefx-medium.onnx differ diff --git a/tts/voices/es_ES-davefx-medium.onnx.json b/tts/voices/es_ES-davefx-medium.onnx.json new file mode 100644 index 0000000..4d3bb33 --- /dev/null +++ b/tts/voices/es_ES-davefx-medium.onnx.json @@ -0,0 +1,487 @@ +{ + "audio": { + "sample_rate": 22050, + "quality": "medium" + }, + "espeak": { + "voice": "es" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + "_": [ + 0 + ], + "^": [ + 1 + ], + "$": [ + 2 + ], + " ": [ + 3 + ], + "!": [ + 4 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "β": [ + 125 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "ⱱ": [ + 129 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + "̧": [ + 140 + ], + "̃": [ + 141 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̩": [ + 144 + ], + "ʰ": [ + 145 + ], + "ˤ": [ + 146 + ], + "ε": [ + 147 + ], + "↓": [ + 148 + ], + "#": [ + 149 + ], + "\"": [ + 150 + ], + "↑": [ + 151 + ] + }, + "num_symbols": 256, + "num_speakers": 1, + "speaker_id_map": {}, + "piper_version": "1.0.0", + "language": { + "code": "es_ES", + "family": "es", + "region": "ES", + "name_native": "Español", + "name_english": "Spanish", + "country_english": "Spain" + }, + "dataset": "davefx" +} \ No newline at end of file diff --git a/tts/voices/fr_FR-siwis-medium.onnx b/tts/voices/fr_FR-siwis-medium.onnx new file mode 100644 index 0000000..55c9ba5 Binary files /dev/null and b/tts/voices/fr_FR-siwis-medium.onnx differ diff --git a/tts/voices/fr_FR-siwis-medium.onnx.json b/tts/voices/fr_FR-siwis-medium.onnx.json new file mode 100644 index 0000000..ecb3b00 --- /dev/null +++ b/tts/voices/fr_FR-siwis-medium.onnx.json @@ -0,0 +1,493 @@ +{ + "audio": { + "sample_rate": 22050, + "quality": "medium" + }, + "espeak": { + "voice": "fr" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + "_": [ + 0 + ], + "^": [ + 1 + ], + "$": [ + 2 + ], + " ": [ + 3 + ], + "!": [ + 4 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "β": [ + 125 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "ⱱ": [ + 129 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + "̧": [ + 140 + ], + "̃": [ + 141 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̩": [ + 144 + ], + "ʰ": [ + 145 + ], + "ˤ": [ + 146 + ], + "ε": [ + 147 + ], + "↓": [ + 148 + ], + "#": [ + 149 + ], + "\"": [ + 150 + ], + "↑": [ + 151 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ] + }, + "num_symbols": 256, + "num_speakers": 1, + "speaker_id_map": {}, + "piper_version": "1.0.0", + "language": { + "code": "fr_FR", + "family": "fr", + "region": "FR", + "name_native": "Français", + "name_english": "French", + "country_english": "France" + }, + "dataset": "siwis" +} \ No newline at end of file diff --git a/tts/voices/it_IT-riccardo-x_low.onnx b/tts/voices/it_IT-riccardo-x_low.onnx new file mode 100644 index 0000000..b9d2489 Binary files /dev/null and b/tts/voices/it_IT-riccardo-x_low.onnx differ diff --git a/tts/voices/it_IT-riccardo-x_low.onnx.json b/tts/voices/it_IT-riccardo-x_low.onnx.json new file mode 100644 index 0000000..8412958 --- /dev/null +++ b/tts/voices/it_IT-riccardo-x_low.onnx.json @@ -0,0 +1,420 @@ +{ + "audio": { + "sample_rate": 16000, + "quality": "x_low" + }, + "espeak": { + "voice": "it" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_map": {}, + "phoneme_id_map": { + "_": [ + 0 + ], + "^": [ + 1 + ], + "$": [ + 2 + ], + " ": [ + 3 + ], + "!": [ + 4 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "β": [ + 125 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "ⱱ": [ + 129 + ] + }, + "num_symbols": 130, + "num_speakers": 1, + "speaker_id_map": {}, + "piper_version": "0.2.0", + "language": { + "code": "it_IT", + "family": "it", + "region": "IT", + "name_native": "Italiano", + "name_english": "Italian", + "country_english": "Italy" + }, + "dataset": "riccardo" +} \ No newline at end of file