generated from fahricansecer/boilerplate-be
main
This commit is contained in:
56
package-lock.json
generated
56
package-lock.json
generated
@@ -42,7 +42,7 @@
|
|||||||
"prisma": "^5.22.0",
|
"prisma": "^5.22.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
@@ -1137,7 +1137,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -3075,7 +3074,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -3242,7 +3240,6 @@
|
|||||||
"version": "11.1.11",
|
"version": "11.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.11.tgz",
|
||||||
"integrity": "sha512-R/+A8XFqLgN8zNs2twhrOaE7dJbRQhdPX3g46am4RT/x8xGLqDphrXkUIno4cGUZHxbczChBAaAPTdPv73wDZA==",
|
"integrity": "sha512-R/+A8XFqLgN8zNs2twhrOaE7dJbRQhdPX3g46am4RT/x8xGLqDphrXkUIno4cGUZHxbczChBAaAPTdPv73wDZA==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"file-type": "21.2.0",
|
"file-type": "21.2.0",
|
||||||
"iterare": "1.2.1",
|
"iterare": "1.2.1",
|
||||||
@@ -3288,7 +3285,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.11.tgz",
|
||||||
"integrity": "sha512-H9i+zT3RvHi7tDc+lCmWHJ3ustXveABCr+Vcpl96dNOxgmrx4elQSTC4W93Mlav2opfLV+p0UTHY6L+bpUA4zA==",
|
"integrity": "sha512-H9i+zT3RvHi7tDc+lCmWHJ3ustXveABCr+Vcpl96dNOxgmrx4elQSTC4W93Mlav2opfLV+p0UTHY6L+bpUA4zA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/opencollective": "0.4.1",
|
"@nuxt/opencollective": "0.4.1",
|
||||||
"fast-safe-stringify": "2.1.1",
|
"fast-safe-stringify": "2.1.1",
|
||||||
@@ -3368,7 +3364,6 @@
|
|||||||
"version": "11.1.11",
|
"version": "11.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.11.tgz",
|
||||||
"integrity": "sha512-kyABSskdMRIAMWL0SlbwtDy4yn59RL4HDdwHDz/fxWuv7/53YP8Y2DtV3/sHqY5Er0msMVTZrM38MjqXhYL7gw==",
|
"integrity": "sha512-kyABSskdMRIAMWL0SlbwtDy4yn59RL4HDdwHDz/fxWuv7/53YP8Y2DtV3/sHqY5Er0msMVTZrM38MjqXhYL7gw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
@@ -3389,7 +3384,6 @@
|
|||||||
"version": "11.1.11",
|
"version": "11.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.11.tgz",
|
||||||
"integrity": "sha512-0z6pLg9CuTXtz7q2lRZoPOU94DN28OTa39f4cQrlZysKA6QrKM7w7z6xqb4g32qjF+LQHFNRmMJtE/pLrxBaig==",
|
"integrity": "sha512-0z6pLg9CuTXtz7q2lRZoPOU94DN28OTa39f4cQrlZysKA6QrKM7w7z6xqb4g32qjF+LQHFNRmMJtE/pLrxBaig==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"socket.io": "4.8.3",
|
"socket.io": "4.8.3",
|
||||||
"tslib": "2.8.1"
|
"tslib": "2.8.1"
|
||||||
@@ -3724,7 +3718,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
|
||||||
"integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
|
"integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.13"
|
"node": ">=16.13"
|
||||||
},
|
},
|
||||||
@@ -3789,7 +3782,6 @@
|
|||||||
"version": "1.6.1",
|
"version": "1.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
|
||||||
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
|
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cluster-key-slot": "1.1.2",
|
"cluster-key-slot": "1.1.2",
|
||||||
"generic-pool": "3.9.0",
|
"generic-pool": "3.9.0",
|
||||||
@@ -4695,7 +4687,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||||
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
|
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "*",
|
"@types/estree": "*",
|
||||||
"@types/json-schema": "*"
|
"@types/json-schema": "*"
|
||||||
@@ -4810,7 +4801,6 @@
|
|||||||
"version": "22.19.3",
|
"version": "22.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
|
||||||
"integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
|
"integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -4975,7 +4965,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.52.0.tgz",
|
||||||
"integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==",
|
"integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.52.0",
|
"@typescript-eslint/scope-manager": "8.52.0",
|
||||||
"@typescript-eslint/types": "8.52.0",
|
"@typescript-eslint/types": "8.52.0",
|
||||||
@@ -5613,7 +5602,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -5667,7 +5655,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -6157,7 +6144,6 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -6231,7 +6217,6 @@
|
|||||||
"version": "5.66.4",
|
"version": "5.66.4",
|
||||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.66.4.tgz",
|
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.66.4.tgz",
|
||||||
"integrity": "sha512-y2VRk2z7d1YNI2JQDD7iThoD0X/0iZZ3VEp8lqT5s5U0XDl9CIjXp1LQgmE9EKy6ReHtzmYXS1f328PnUbZGtQ==",
|
"integrity": "sha512-y2VRk2z7d1YNI2JQDD7iThoD0X/0iZZ3VEp8lqT5s5U0XDl9CIjXp1LQgmE9EKy6ReHtzmYXS1f328PnUbZGtQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cron-parser": "4.9.0",
|
"cron-parser": "4.9.0",
|
||||||
"ioredis": "5.8.2",
|
"ioredis": "5.8.2",
|
||||||
@@ -6305,7 +6290,6 @@
|
|||||||
"version": "7.2.7",
|
"version": "7.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.7.tgz",
|
||||||
"integrity": "sha512-TKeeb9nSybk1e9E5yAiPVJ6YKdX9FYhwqqy8fBfVKAFVTJYZUNmeIvwjURW6+UikNsO6l2ta27thYgo/oumDsw==",
|
"integrity": "sha512-TKeeb9nSybk1e9E5yAiPVJ6YKdX9FYhwqqy8fBfVKAFVTJYZUNmeIvwjURW6+UikNsO6l2ta27thYgo/oumDsw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cacheable/utils": "^2.3.2",
|
"@cacheable/utils": "^2.3.2",
|
||||||
"keyv": "^5.5.4"
|
"keyv": "^5.5.4"
|
||||||
@@ -6457,7 +6441,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readdirp": "^4.0.1"
|
"readdirp": "^4.0.1"
|
||||||
},
|
},
|
||||||
@@ -6501,14 +6484,12 @@
|
|||||||
"node_modules/class-transformer": {
|
"node_modules/class-transformer": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/class-validator": {
|
"node_modules/class-validator": {
|
||||||
"version": "0.14.3",
|
"version": "0.14.3",
|
||||||
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz",
|
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz",
|
||||||
"integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==",
|
"integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/validator": "^13.15.3",
|
"@types/validator": "^13.15.3",
|
||||||
"libphonenumber-js": "^1.11.1",
|
"libphonenumber-js": "^1.11.1",
|
||||||
@@ -7194,7 +7175,8 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
|
||||||
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
|
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/es-object-atoms": {
|
"node_modules/es-object-atoms": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -7253,7 +7235,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -7313,7 +7294,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
|
||||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"eslint-config-prettier": "bin/cli.js"
|
"eslint-config-prettier": "bin/cli.js"
|
||||||
},
|
},
|
||||||
@@ -8674,7 +8654,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz",
|
||||||
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "30.2.0",
|
"@jest/core": "30.2.0",
|
||||||
"@jest/types": "30.2.0",
|
"@jest/types": "30.2.0",
|
||||||
@@ -9518,7 +9497,6 @@
|
|||||||
"version": "5.5.5",
|
"version": "5.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.5.tgz",
|
||||||
"integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==",
|
"integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@keyv/serialize": "^1.1.1"
|
"@keyv/serialize": "^1.1.1"
|
||||||
}
|
}
|
||||||
@@ -10263,6 +10241,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
@@ -10446,7 +10425,6 @@
|
|||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
||||||
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"passport-strategy": "1.x.x",
|
"passport-strategy": "1.x.x",
|
||||||
"pause": "0.0.1",
|
"pause": "0.0.1",
|
||||||
@@ -10573,7 +10551,6 @@
|
|||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz",
|
||||||
"integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==",
|
"integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pinojs/redact": "^0.4.0",
|
"@pinojs/redact": "^0.4.0",
|
||||||
"atomic-sleep": "^1.0.0",
|
"atomic-sleep": "^1.0.0",
|
||||||
@@ -10603,7 +10580,6 @@
|
|||||||
"version": "11.0.0",
|
"version": "11.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pino-http/-/pino-http-11.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pino-http/-/pino-http-11.0.0.tgz",
|
||||||
"integrity": "sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==",
|
"integrity": "sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-caller-file": "^2.0.5",
|
"get-caller-file": "^2.0.5",
|
||||||
"pino": "^10.0.0",
|
"pino": "^10.0.0",
|
||||||
@@ -10757,7 +10733,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
||||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@@ -10811,7 +10786,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
|
||||||
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
|
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/engines": "5.22.0"
|
"@prisma/engines": "5.22.0"
|
||||||
},
|
},
|
||||||
@@ -11876,7 +11850,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -12191,7 +12164,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cspotcode/source-map-support": "^0.8.0",
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
"@tsconfig/node10": "^1.0.7",
|
"@tsconfig/node10": "^1.0.7",
|
||||||
@@ -12329,7 +12301,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -12668,6 +12639,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^8.0.0"
|
"ajv": "^8.0.0"
|
||||||
},
|
},
|
||||||
@@ -12685,6 +12657,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
|
||||||
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3"
|
"fast-deep-equal": "^3.1.3"
|
||||||
},
|
},
|
||||||
@@ -12697,6 +12670,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||||
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
|
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esrecurse": "^4.3.0",
|
"esrecurse": "^4.3.0",
|
||||||
"estraverse": "^4.1.1"
|
"estraverse": "^4.1.1"
|
||||||
@@ -12710,6 +12684,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
|
||||||
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
|
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
@@ -12718,13 +12693,15 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/webpack/node_modules/mime-db": {
|
"node_modules/webpack/node_modules/mime-db": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@@ -12734,6 +12711,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
},
|
},
|
||||||
@@ -12746,6 +12724,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
|
||||||
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
|
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/json-schema": "^7.0.9",
|
"@types/json-schema": "^7.0.9",
|
||||||
"ajv": "^8.9.0",
|
"ajv": "^8.9.0",
|
||||||
@@ -12953,9 +12932,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.3.5",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||||
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
"prisma": "^5.22.0",
|
"prisma": "^5.22.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
|
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { GoogleGenAI } from '@google/genai';
|
import { GoogleGenAI } from '@google/genai';
|
||||||
|
import { ZodSchema, ZodError } from 'zod';
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Types & Interfaces
|
||||||
|
// ============================================
|
||||||
|
|
||||||
export interface GeminiGenerateOptions {
|
export interface GeminiGenerateOptions {
|
||||||
model?: string;
|
model?: string;
|
||||||
@@ -15,30 +20,72 @@ export interface GeminiChatMessage {
|
|||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GeminiJSONOptions<T = any> extends GeminiGenerateOptions {
|
||||||
|
/** Zod schema for runtime validation of the AI response */
|
||||||
|
zodSchema?: ZodSchema<T>;
|
||||||
|
/** Max retry attempts for JSON generation (default: 3) */
|
||||||
|
maxRetries?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gemini AI Service
|
* Error types for Gemini API failures
|
||||||
|
*/
|
||||||
|
export enum GeminiErrorType {
|
||||||
|
RATE_LIMIT = 'RATE_LIMIT',
|
||||||
|
QUOTA_EXCEEDED = 'QUOTA_EXCEEDED',
|
||||||
|
SAFETY_BLOCKED = 'SAFETY_BLOCKED',
|
||||||
|
INVALID_RESPONSE = 'INVALID_RESPONSE',
|
||||||
|
JSON_PARSE_FAILED = 'JSON_PARSE_FAILED',
|
||||||
|
TIMEOUT = 'TIMEOUT',
|
||||||
|
UNAVAILABLE = 'UNAVAILABLE',
|
||||||
|
UNKNOWN = 'UNKNOWN',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom exception for Gemini AI errors with rich context
|
||||||
|
*/
|
||||||
|
export class GeminiException extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly type: GeminiErrorType,
|
||||||
|
public readonly originalError?: any,
|
||||||
|
public readonly retryable: boolean = false,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'GeminiException';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Service
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gemini AI Service — Enhanced with Retry, JSON Recovery & Validation
|
||||||
*
|
*
|
||||||
* Provides AI-powered text generation using Google Gemini API.
|
* Provides AI-powered text/JSON/image generation using Google Gemini API.
|
||||||
* This service is globally available when ENABLE_GEMINI=true.
|
* This service is globally available when ENABLE_GEMINI=true.
|
||||||
*
|
*
|
||||||
|
* Key improvements over v1:
|
||||||
|
* - responseMimeType: "application/json" for native JSON output
|
||||||
|
* - Exponential backoff retry (up to 3 attempts)
|
||||||
|
* - Multi-strategy JSON extraction & recovery
|
||||||
|
* - Optional Zod schema validation
|
||||||
|
* - Typed GeminiException with error classification
|
||||||
|
* - AI usage metrics logging
|
||||||
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // Simple text generation
|
* // Simple text generation
|
||||||
* const response = await geminiService.generateText('Write a poem about coding');
|
* const response = await geminiService.generateText('Write a poem about coding');
|
||||||
*
|
*
|
||||||
* // With options
|
* // JSON generation with Zod validation
|
||||||
* const response = await geminiService.generateText('Translate to Turkish', {
|
* import { z } from 'zod';
|
||||||
* temperature: 0.7,
|
* const schema = z.object({ title: z.string(), score: z.number() });
|
||||||
* systemPrompt: 'You are a professional translator',
|
* const result = await geminiService.generateJSON(
|
||||||
* });
|
* 'Analyze this script', '{ title, score }',
|
||||||
*
|
* { zodSchema: schema }
|
||||||
* // Chat conversation
|
* );
|
||||||
* const messages = [
|
|
||||||
* { role: 'user', content: 'Hello!' },
|
|
||||||
* { role: 'model', content: 'Hi there!' },
|
|
||||||
* { role: 'user', content: 'What is 2+2?' },
|
|
||||||
* ];
|
|
||||||
* const response = await geminiService.chat(messages);
|
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -87,6 +134,10 @@ export class GeminiService implements OnModuleInit {
|
|||||||
return this.isEnabled && this.client !== null;
|
return this.isEnabled && this.client !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Text Generation
|
||||||
|
// ============================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate text content from a prompt
|
* Generate text content from a prompt
|
||||||
*
|
*
|
||||||
@@ -98,11 +149,10 @@ export class GeminiService implements OnModuleInit {
|
|||||||
prompt: string,
|
prompt: string,
|
||||||
options: GeminiGenerateOptions = {},
|
options: GeminiGenerateOptions = {},
|
||||||
): Promise<{ text: string; usage?: any }> {
|
): Promise<{ text: string; usage?: any }> {
|
||||||
if (!this.isAvailable()) {
|
this.ensureAvailable();
|
||||||
throw new Error('Gemini AI is not available. Check your configuration.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = options.model || this.defaultModel;
|
const model = options.model || this.defaultModel;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contents: any[] = [];
|
const contents: any[] = [];
|
||||||
@@ -134,16 +184,27 @@ export class GeminiService implements OnModuleInit {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - startTime;
|
||||||
|
this.logUsage('generateText', model, response.usageMetadata, durationMs);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: (response.text || '').trim(),
|
text: (response.text || '').trim(),
|
||||||
usage: response.usageMetadata,
|
usage: response.usageMetadata,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Gemini generation failed', error);
|
const durationMs = Date.now() - startTime;
|
||||||
throw error;
|
this.logger.error(
|
||||||
|
`Gemini generation failed after ${durationMs}ms`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
throw this.classifyError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Chat
|
||||||
|
// ============================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Have a multi-turn chat conversation
|
* Have a multi-turn chat conversation
|
||||||
*
|
*
|
||||||
@@ -155,11 +216,10 @@ export class GeminiService implements OnModuleInit {
|
|||||||
messages: GeminiChatMessage[],
|
messages: GeminiChatMessage[],
|
||||||
options: GeminiGenerateOptions = {},
|
options: GeminiGenerateOptions = {},
|
||||||
): Promise<{ text: string; usage?: any }> {
|
): Promise<{ text: string; usage?: any }> {
|
||||||
if (!this.isAvailable()) {
|
this.ensureAvailable();
|
||||||
throw new Error('Gemini AI is not available. Check your configuration.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = options.model || this.defaultModel;
|
const model = options.model || this.defaultModel;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contents = messages.map((msg) => ({
|
const contents = messages.map((msg) => ({
|
||||||
@@ -190,29 +250,54 @@ export class GeminiService implements OnModuleInit {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - startTime;
|
||||||
|
this.logUsage('chat', model, response.usageMetadata, durationMs);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: (response.text || '').trim(),
|
text: (response.text || '').trim(),
|
||||||
usage: response.usageMetadata,
|
usage: response.usageMetadata,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Gemini chat failed', error);
|
this.logger.error('Gemini chat failed', error);
|
||||||
throw error;
|
throw this.classifyError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// JSON Generation (Enhanced)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate structured JSON output
|
* Generate structured JSON output with retry, recovery, and optional Zod validation.
|
||||||
|
*
|
||||||
|
* Strategy:
|
||||||
|
* 1. First attempt uses `responseMimeType: "application/json"` for native JSON
|
||||||
|
* 2. If that fails, falls back to prompt-based JSON with multi-strategy extraction
|
||||||
|
* 3. Up to `maxRetries` attempts with exponential backoff
|
||||||
|
* 4. Optional Zod schema validation on the parsed result
|
||||||
*
|
*
|
||||||
* @param prompt - The prompt describing what JSON to generate
|
* @param prompt - The prompt describing what JSON to generate
|
||||||
* @param schema - JSON schema description for the expected output
|
* @param schema - JSON schema description for the expected output (human readable)
|
||||||
* @param options - Optional configuration for the generation
|
* @param options - Optional configuration including zodSchema and maxRetries
|
||||||
* @returns Parsed JSON object
|
* @returns Parsed and optionally validated JSON object
|
||||||
*/
|
*/
|
||||||
async generateJSON<T = any>(
|
async generateJSON<T = any>(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
schema: string,
|
schema: string,
|
||||||
options: GeminiGenerateOptions = {},
|
options: GeminiJSONOptions<T> = {},
|
||||||
): Promise<{ data: T; usage?: any }> {
|
): Promise<{ data: T; usage?: any }> {
|
||||||
|
this.ensureAvailable();
|
||||||
|
|
||||||
|
const maxRetries = options.maxRetries ?? 3;
|
||||||
|
const model = options.model || this.defaultModel;
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
let lastUsage: any = undefined;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Build the full prompt
|
||||||
const fullPrompt = `${prompt}
|
const fullPrompt = `${prompt}
|
||||||
|
|
||||||
Output the result as valid JSON that matches this schema:
|
Output the result as valid JSON that matches this schema:
|
||||||
@@ -220,25 +305,110 @@ ${schema}
|
|||||||
|
|
||||||
IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
|
IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
|
||||||
|
|
||||||
const response = await this.generateText(fullPrompt, options);
|
const contents: any[] = [];
|
||||||
|
|
||||||
try {
|
if (options.systemPrompt) {
|
||||||
// Try to extract JSON from the response
|
contents.push({
|
||||||
let jsonStr = response.text;
|
role: 'user',
|
||||||
|
parts: [{ text: options.systemPrompt }],
|
||||||
// Remove potential markdown code blocks
|
});
|
||||||
const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
|
contents.push({
|
||||||
if (jsonMatch) {
|
role: 'model',
|
||||||
jsonStr = jsonMatch[1].trim();
|
parts: [
|
||||||
|
{ text: 'Understood. I will follow these instructions.' },
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contents.push({
|
||||||
|
role: 'user',
|
||||||
|
parts: [{ text: fullPrompt }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure responseMimeType for native JSON (only when no tools — tools don't support it)
|
||||||
|
const config: any = {
|
||||||
|
temperature: options.temperature,
|
||||||
|
maxOutputTokens: options.maxTokens,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!options.tools || options.tools.length === 0) {
|
||||||
|
config.responseMimeType = 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.tools) {
|
||||||
|
config.tools = options.tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.client!.models.generateContent({
|
||||||
|
model,
|
||||||
|
contents,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - startTime;
|
||||||
|
lastUsage = response.usageMetadata;
|
||||||
|
this.logUsage(
|
||||||
|
`generateJSON (attempt ${attempt}/${maxRetries})`,
|
||||||
|
model,
|
||||||
|
response.usageMetadata,
|
||||||
|
durationMs,
|
||||||
|
);
|
||||||
|
|
||||||
|
const rawText = (response.text || '').trim();
|
||||||
|
|
||||||
|
// Try to extract and parse JSON
|
||||||
|
const jsonStr = this.extractJSON(rawText);
|
||||||
const data = JSON.parse(jsonStr) as T;
|
const data = JSON.parse(jsonStr) as T;
|
||||||
return { data, usage: response.usage };
|
|
||||||
|
// Validate with Zod schema if provided
|
||||||
|
if (options.zodSchema) {
|
||||||
|
const validated = options.zodSchema.parse(data);
|
||||||
|
return { data: validated as T, usage: lastUsage };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data, usage: lastUsage };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Failed to parse JSON response', error);
|
lastError = error as Error;
|
||||||
throw new Error('Failed to parse AI response as JSON');
|
|
||||||
|
const isParseError =
|
||||||
|
error instanceof SyntaxError ||
|
||||||
|
(error instanceof ZodError) ||
|
||||||
|
(error instanceof Error &&
|
||||||
|
error.message.includes('Failed to extract JSON'));
|
||||||
|
|
||||||
|
const isRetryable = isParseError || this.isRetryableError(error);
|
||||||
|
|
||||||
|
if (isRetryable && attempt < maxRetries) {
|
||||||
|
const backoffMs = Math.min(1000 * Math.pow(2, attempt - 1), 8000);
|
||||||
|
this.logger.warn(
|
||||||
|
`JSON generation attempt ${attempt}/${maxRetries} failed (${error instanceof Error ? error.message : 'unknown'}). Retrying in ${backoffMs}ms...`,
|
||||||
|
);
|
||||||
|
await this.sleep(backoffMs);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log failure details
|
||||||
|
if (error instanceof ZodError) {
|
||||||
|
this.logger.error(
|
||||||
|
`Zod validation failed after ${attempt} attempts: ${error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All retries exhausted
|
||||||
|
throw new GeminiException(
|
||||||
|
`Failed to generate valid JSON after ${maxRetries} attempts: ${lastError?.message}`,
|
||||||
|
GeminiErrorType.JSON_PARSE_FAILED,
|
||||||
|
lastError,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Image Generation
|
||||||
|
// ============================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate an image using Google Imagen (Nano Banana)
|
* Generate an image using Google Imagen (Nano Banana)
|
||||||
*
|
*
|
||||||
@@ -246,9 +416,9 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
|
|||||||
* @returns Base64 encoded image data URI
|
* @returns Base64 encoded image data URI
|
||||||
*/
|
*/
|
||||||
async generateImage(prompt: string): Promise<string> {
|
async generateImage(prompt: string): Promise<string> {
|
||||||
if (!this.isAvailable()) {
|
this.ensureAvailable();
|
||||||
throw new Error('Gemini AI is not available. Check your configuration.');
|
|
||||||
}
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use Imagen 3.0 (Nano Banana Pro)
|
// Use Imagen 3.0 (Nano Banana Pro)
|
||||||
@@ -263,6 +433,11 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
|
|||||||
},
|
},
|
||||||
})) as any;
|
})) as any;
|
||||||
|
|
||||||
|
const durationMs = Date.now() - startTime;
|
||||||
|
this.logger.log(
|
||||||
|
`Image generated in ${durationMs}ms (model: ${model})`,
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
response.images &&
|
response.images &&
|
||||||
response.images.length > 0 &&
|
response.images.length > 0 &&
|
||||||
@@ -272,11 +447,209 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
|
|||||||
return `data:image/png;base64,${response.images[0].image}`;
|
return `data:image/png;base64,${response.images[0].image}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('No image returned from Gemini');
|
throw new GeminiException(
|
||||||
|
'No image returned from Gemini',
|
||||||
|
GeminiErrorType.INVALID_RESPONSE,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof GeminiException) throw error;
|
||||||
this.logger.error('Gemini image generation failed', error);
|
this.logger.error('Gemini image generation failed', error);
|
||||||
// Fallback or rethrow
|
throw this.classifyError(error);
|
||||||
throw error;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Private Helpers
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure Gemini client is available, throw typed exception if not
|
||||||
|
*/
|
||||||
|
private ensureAvailable(): void {
|
||||||
|
if (!this.isAvailable()) {
|
||||||
|
throw new GeminiException(
|
||||||
|
'Gemini AI is not available. Check your configuration.',
|
||||||
|
GeminiErrorType.UNAVAILABLE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract JSON from a raw AI response using multiple strategies:
|
||||||
|
* 1. Direct parse (cleanest case)
|
||||||
|
* 2. Strip markdown code blocks
|
||||||
|
* 3. Find first { or [ and match to closing bracket
|
||||||
|
* 4. Remove trailing commas and retry
|
||||||
|
*/
|
||||||
|
private extractJSON(raw: string): string {
|
||||||
|
// Strategy 1: Direct parse attempt
|
||||||
|
try {
|
||||||
|
JSON.parse(raw);
|
||||||
|
return raw;
|
||||||
|
} catch {
|
||||||
|
// Continue to next strategy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Strip markdown code blocks (```json ... ``` or ``` ... ```)
|
||||||
|
const codeBlockMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||||
|
if (codeBlockMatch) {
|
||||||
|
const extracted = codeBlockMatch[1].trim();
|
||||||
|
try {
|
||||||
|
JSON.parse(extracted);
|
||||||
|
return extracted;
|
||||||
|
} catch {
|
||||||
|
// Continue with the extracted content for further cleaning
|
||||||
|
raw = extracted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 3: Find the first { or [ and match to the last } or ]
|
||||||
|
const objectStart = raw.indexOf('{');
|
||||||
|
const arrayStart = raw.indexOf('[');
|
||||||
|
|
||||||
|
let start = -1;
|
||||||
|
let endChar = '';
|
||||||
|
|
||||||
|
if (objectStart >= 0 && (arrayStart < 0 || objectStart < arrayStart)) {
|
||||||
|
start = objectStart;
|
||||||
|
endChar = '}';
|
||||||
|
} else if (arrayStart >= 0) {
|
||||||
|
start = arrayStart;
|
||||||
|
endChar = ']';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start >= 0) {
|
||||||
|
const end = raw.lastIndexOf(endChar);
|
||||||
|
if (end > start) {
|
||||||
|
const candidate = raw.substring(start, end + 1);
|
||||||
|
try {
|
||||||
|
JSON.parse(candidate);
|
||||||
|
return candidate;
|
||||||
|
} catch {
|
||||||
|
// Strategy 4: Remove trailing commas and retry
|
||||||
|
const cleaned = candidate
|
||||||
|
.replace(/,\s*([\]}])/g, '$1') // Remove trailing commas
|
||||||
|
.replace(/'/g, '"') // Replace single quotes with double quotes
|
||||||
|
.replace(/(\w+)\s*:/g, '"$1":') // Quote unquoted keys
|
||||||
|
.replace(/""(\w+)""/g, '"$1"'); // Fix double-quoted keys
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSON.parse(cleaned);
|
||||||
|
return cleaned;
|
||||||
|
} catch {
|
||||||
|
// Last resort: return the candidate anyway, caller will handle error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Failed to extract JSON from AI response (length: ${raw.length})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify an error into a typed GeminiException
|
||||||
|
*/
|
||||||
|
private classifyError(error: any): GeminiException {
|
||||||
|
if (error instanceof GeminiException) return error;
|
||||||
|
|
||||||
|
const message = error?.message || String(error);
|
||||||
|
const status = error?.status || error?.statusCode;
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
if (status === 429 || message.includes('429') || message.includes('RATE_LIMIT') || message.includes('rate limit')) {
|
||||||
|
return new GeminiException(
|
||||||
|
'Gemini API rate limit exceeded. Please wait before retrying.',
|
||||||
|
GeminiErrorType.RATE_LIMIT,
|
||||||
|
error,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quota
|
||||||
|
if (message.includes('QUOTA') || message.includes('quota') || status === 403) {
|
||||||
|
return new GeminiException(
|
||||||
|
'Gemini API quota exceeded.',
|
||||||
|
GeminiErrorType.QUOTA_EXCEEDED,
|
||||||
|
error,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety
|
||||||
|
if (message.includes('SAFETY') || message.includes('safety') || message.includes('blocked')) {
|
||||||
|
return new GeminiException(
|
||||||
|
'Content was blocked by safety filters. Try rephrasing the prompt.',
|
||||||
|
GeminiErrorType.SAFETY_BLOCKED,
|
||||||
|
error,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout
|
||||||
|
if (message.includes('TIMEOUT') || message.includes('timeout') || message.includes('DEADLINE_EXCEEDED')) {
|
||||||
|
return new GeminiException(
|
||||||
|
'Gemini API request timed out.',
|
||||||
|
GeminiErrorType.TIMEOUT,
|
||||||
|
error,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic
|
||||||
|
return new GeminiException(
|
||||||
|
`Gemini API error: ${message}`,
|
||||||
|
GeminiErrorType.UNKNOWN,
|
||||||
|
error,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an error is retryable
|
||||||
|
*/
|
||||||
|
private isRetryableError(error: any): boolean {
|
||||||
|
if (error instanceof GeminiException) return error.retryable;
|
||||||
|
const message = error?.message || '';
|
||||||
|
return (
|
||||||
|
message.includes('429') ||
|
||||||
|
message.includes('RATE_LIMIT') ||
|
||||||
|
message.includes('TIMEOUT') ||
|
||||||
|
message.includes('DEADLINE_EXCEEDED') ||
|
||||||
|
message.includes('UNAVAILABLE') ||
|
||||||
|
message.includes('INTERNAL')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log AI usage metrics for monitoring
|
||||||
|
*/
|
||||||
|
private logUsage(
|
||||||
|
operation: string,
|
||||||
|
model: string,
|
||||||
|
usage: any,
|
||||||
|
durationMs: number,
|
||||||
|
): void {
|
||||||
|
if (usage) {
|
||||||
|
this.logger.log(
|
||||||
|
`AI Usage [${operation}] model=${model} ` +
|
||||||
|
`prompt=${usage.promptTokenCount || '?'} ` +
|
||||||
|
`completion=${usage.candidatesTokenCount || '?'} ` +
|
||||||
|
`total=${usage.totalTokenCount || '?'} ` +
|
||||||
|
`duration=${durationMs}ms`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`AI Usage [${operation}] model=${model} duration=${durationMs}ms`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sleep helper for retry backoff
|
||||||
|
*/
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
191
src/modules/skriptai/prompts/analysis.prompt.ts
Normal file
191
src/modules/skriptai/prompts/analysis.prompt.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/**
|
||||||
|
* Analysis Prompt Builders
|
||||||
|
*
|
||||||
|
* Prompts for AI-powered content analysis:
|
||||||
|
* - Neuro Marketing Analysis (Cialdini's 6 Principles)
|
||||||
|
* - YouTube Algorithm Audit
|
||||||
|
* - Commercial Brief (Sponsorship Analysis)
|
||||||
|
* - Visual Asset Keywords
|
||||||
|
*
|
||||||
|
* Used in: AnalysisService
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Neuro Marketing Analysis
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface NeuroAnalysisInput {
|
||||||
|
fullScript: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildNeuroAnalysisPrompt(input: NeuroAnalysisInput): {
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
schema: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
prompt: `Analyze this script using Consumer Neuroscience and Cialdini's 6 Principles of Persuasion.
|
||||||
|
|
||||||
|
Script:
|
||||||
|
${input.fullScript.substring(0, 10000)}
|
||||||
|
|
||||||
|
Provide:
|
||||||
|
1. Engagement Score (0-100): How well does it capture attention?
|
||||||
|
2. Dopamine Score (0-100): Does it create anticipation & reward loops?
|
||||||
|
3. Clarity Score (0-100): Is the message clear and memorable?
|
||||||
|
|
||||||
|
4. Cialdini's Persuasion Metrics (0-100 each):
|
||||||
|
- Reciprocity: Does it give value first?
|
||||||
|
- Scarcity: Does it create urgency?
|
||||||
|
- Authority: Does it establish credibility?
|
||||||
|
- Consistency: Does it align with viewer beliefs?
|
||||||
|
- Liking: Is the tone likeable/relatable?
|
||||||
|
- Social Proof: Does it reference others' actions?
|
||||||
|
|
||||||
|
5. Neuro Metrics:
|
||||||
|
- Attention Hooks: Moments that grab attention
|
||||||
|
- Emotional Triggers: Points that evoke emotion
|
||||||
|
- Memory Anchors: Unique/memorable elements
|
||||||
|
- Action Drivers: CTAs or challenges
|
||||||
|
|
||||||
|
6. Suggestions: 3-5 specific improvements`,
|
||||||
|
|
||||||
|
temperature: 0.6,
|
||||||
|
|
||||||
|
schema: `{
|
||||||
|
"engagementScore": 0,
|
||||||
|
"dopamineScore": 0,
|
||||||
|
"clarityScore": 0,
|
||||||
|
"persuasionMetrics": {
|
||||||
|
"reciprocity": 0, "scarcity": 0, "authority": 0,
|
||||||
|
"consistency": 0, "liking": 0, "socialProof": 0
|
||||||
|
},
|
||||||
|
"neuroMetrics": {
|
||||||
|
"attentionHooks": ["..."], "emotionalTriggers": ["..."],
|
||||||
|
"memoryAnchors": ["..."], "actionDrivers": ["..."]
|
||||||
|
},
|
||||||
|
"suggestions": ["..."]
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// YouTube Audit
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface YoutubeAuditInput {
|
||||||
|
topic: string;
|
||||||
|
fullScript: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildYoutubeAuditPrompt(input: YoutubeAuditInput): {
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
schema: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
prompt: `Perform a YouTube Algorithm Audit on this script for topic "${input.topic}".
|
||||||
|
|
||||||
|
Script:
|
||||||
|
${input.fullScript.substring(0, 10000)}
|
||||||
|
|
||||||
|
Analyze and provide:
|
||||||
|
1. Hook Score (0-100): First 10 seconds effectiveness
|
||||||
|
2. Pacing Score (0-100): Does it maintain momentum?
|
||||||
|
3. Viral Potential (0-100): Shareability factor
|
||||||
|
|
||||||
|
4. Retention Analysis: 3-5 potential drop-off points with time, issue, suggestion, severity (High/Medium/Low)
|
||||||
|
|
||||||
|
5. Thumbnail Concepts: 3 high-CTR thumbnail ideas with:
|
||||||
|
- Concept name, Visual description, Text overlay
|
||||||
|
- Color psychology, Emotion target, AI generation prompt
|
||||||
|
|
||||||
|
6. Title Options: 5 clickable titles (curiosity gap, numbers, power words)
|
||||||
|
7. Community Post: Engaging post to tease the video
|
||||||
|
8. Pinned Comment: Engagement-driving first comment
|
||||||
|
9. SEO Description: Optimized video description with keywords
|
||||||
|
10. Keywords: 10 relevant search keywords`,
|
||||||
|
|
||||||
|
temperature: 0.7,
|
||||||
|
|
||||||
|
schema: `{
|
||||||
|
"hookScore": 0, "pacingScore": 0, "viralPotential": 0,
|
||||||
|
"retentionAnalysis": [{ "time": "0:30", "issue": "...", "suggestion": "...", "severity": "High" }],
|
||||||
|
"thumbnails": [{ "conceptName": "...", "visualDescription": "...", "textOverlay": "...", "colorPsychology": "...", "emotionTarget": "...", "aiPrompt": "..." }],
|
||||||
|
"titles": ["..."],
|
||||||
|
"communityPost": "...", "pinnedComment": "...",
|
||||||
|
"description": "...", "keywords": ["..."]
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Commercial Brief
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface CommercialBriefInput {
|
||||||
|
topic: string;
|
||||||
|
targetAudience: string[];
|
||||||
|
contentType: string;
|
||||||
|
fullScript: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCommercialBriefPrompt(input: CommercialBriefInput): {
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
schema: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
prompt: `Analyze this content for commercial viability and sponsorship opportunities.
|
||||||
|
|
||||||
|
Topic: "${input.topic}"
|
||||||
|
Audience: ${input.targetAudience.join(', ')}
|
||||||
|
Content Type: ${input.contentType}
|
||||||
|
|
||||||
|
Script excerpt:
|
||||||
|
${input.fullScript.substring(0, 5000)}
|
||||||
|
|
||||||
|
Provide:
|
||||||
|
1. Viability Score (1-10 scale as string): "8/10"
|
||||||
|
2. Viability Reason: Why this content is commercially viable
|
||||||
|
|
||||||
|
3. Sponsor Suggestions (3-5 potential sponsors):
|
||||||
|
- Company name, Industry
|
||||||
|
- Match reason (why this sponsor fits)
|
||||||
|
- Email draft (outreach template)`,
|
||||||
|
|
||||||
|
temperature: 0.6,
|
||||||
|
|
||||||
|
schema: `{
|
||||||
|
"viabilityScore": "8/10",
|
||||||
|
"viabilityReason": "...",
|
||||||
|
"sponsors": [{ "name": "...", "industry": "...", "matchReason": "...", "emailDraft": "..." }]
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Visual Asset Keywords
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface VisualAssetKeywordsInput {
|
||||||
|
topic: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildVisualAssetKeywordsPrompt(
|
||||||
|
input: VisualAssetKeywordsInput,
|
||||||
|
): {
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
schema: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
prompt: `Generate ${input.count} specific, simple visual keywords for an image generator about "${input.topic}".
|
||||||
|
Format: "subject action context style". Keep it English, concise, no special chars.`,
|
||||||
|
|
||||||
|
temperature: 0.8,
|
||||||
|
|
||||||
|
schema: '["keyword1", "keyword2", ...]',
|
||||||
|
};
|
||||||
|
}
|
||||||
38
src/modules/skriptai/prompts/character-generation.prompt.ts
Normal file
38
src/modules/skriptai/prompts/character-generation.prompt.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Character Generation Prompt Builder
|
||||||
|
*
|
||||||
|
* Uses Alan C. Hueth's "Triunity of Character" model to create
|
||||||
|
* rich character profiles for video content.
|
||||||
|
*
|
||||||
|
* Used in: ResearchService.generateCharacters()
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface CharacterGenerationInput {
|
||||||
|
contentType: string;
|
||||||
|
topic: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCharacterGenerationPrompt(
|
||||||
|
input: CharacterGenerationInput,
|
||||||
|
): {
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
schema: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
prompt: `Create Character Profiles for a ${input.contentType} about "${input.topic}".
|
||||||
|
Use Alan C. Hueth's "Triunity of Character" model:
|
||||||
|
1. Values (Inner belief)
|
||||||
|
2. Traits (Personality)
|
||||||
|
3. Mannerisms (External behavior)
|
||||||
|
|
||||||
|
If format is non-fiction (Youtube Doc), create a 'Host/Narrator' persona and potentially an 'Antagonist' (e.g., The Problem, Time, A Rival).
|
||||||
|
Language: ${input.language}.`,
|
||||||
|
|
||||||
|
temperature: 0.8,
|
||||||
|
|
||||||
|
schema:
|
||||||
|
'[{ "name": "Name", "role": "Protagonist", "values": "...", "traits": "...", "mannerisms": "..." }]',
|
||||||
|
};
|
||||||
|
}
|
||||||
51
src/modules/skriptai/prompts/deep-research.prompt.ts
Normal file
51
src/modules/skriptai/prompts/deep-research.prompt.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Deep Research Prompt Builders
|
||||||
|
*
|
||||||
|
* Two-stage prompts:
|
||||||
|
* 1. Generate search queries for a topic
|
||||||
|
* 2. Find high-quality web sources for each query
|
||||||
|
*
|
||||||
|
* Used in: ResearchService.performDeepResearch()
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface SearchQueryInput {
|
||||||
|
topic: string;
|
||||||
|
briefContext: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSearchQueryPrompt(input: SearchQueryInput): {
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
schema: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
prompt: `Generate 5 specific Google Search queries for "${input.topic}".
|
||||||
|
Context: ${input.briefContext}. Language: ${input.language}.
|
||||||
|
Return strictly a JSON array of strings.`,
|
||||||
|
|
||||||
|
temperature: 0.7,
|
||||||
|
|
||||||
|
schema: '["query1", "query2", ...]',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SourceSearchInput {
|
||||||
|
query: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSourceSearchPrompt(input: SourceSearchInput): {
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
schema: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
prompt: `Find 3 high-quality web sources for: ${input.query}. Language: ${input.language}.
|
||||||
|
Return JSON array: [{ "title": string, "url": string, "snippet": string, "type": "article" }]`,
|
||||||
|
|
||||||
|
temperature: 0.5,
|
||||||
|
|
||||||
|
schema: '[{ "title": "...", "url": "...", "snippet": "...", "type": "article" }]',
|
||||||
|
};
|
||||||
|
}
|
||||||
53
src/modules/skriptai/prompts/discovery-questions.prompt.ts
Normal file
53
src/modules/skriptai/prompts/discovery-questions.prompt.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Discovery Questions Prompt Builder
|
||||||
|
*
|
||||||
|
* Generates provocative "Screenwriter's Room" style questions
|
||||||
|
* to help shape the narrative arc for a given topic.
|
||||||
|
*
|
||||||
|
* Used in: ResearchService.generateDiscoveryQuestions()
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface DiscoveryQuestionsInput {
|
||||||
|
topic: string;
|
||||||
|
language: string;
|
||||||
|
existingQuestions?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDiscoveryQuestionsPrompt(
|
||||||
|
input: DiscoveryQuestionsInput,
|
||||||
|
): {
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
schema: string;
|
||||||
|
} {
|
||||||
|
const existingContext =
|
||||||
|
input.existingQuestions && input.existingQuestions.length > 0
|
||||||
|
? `Avoid these questions: ${input.existingQuestions.join(', ')}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
prompt: `You are an expert Screenwriter and Creative Director. Topic: "${input.topic}".
|
||||||
|
|
||||||
|
PHASE 1: DEEP DIVE
|
||||||
|
Think like a filmmaker. We are not just making a video; we are telling a story.
|
||||||
|
Analyze the topic "${input.topic}" to find the drama, the conflict, and the human element.
|
||||||
|
|
||||||
|
PHASE 2: INTERROGATION
|
||||||
|
Ask 3-4 provocative, "Screenwriter's Room" style questions to help shape the narrative arc.
|
||||||
|
|
||||||
|
DO NOT ASK: "What is the goal?" or "Who is the audience?".
|
||||||
|
|
||||||
|
INSTEAD ASK (Examples):
|
||||||
|
- "What is the 'Inciting Incident' that makes this topic urgent right now?"
|
||||||
|
- "If this topic was a character, what would be its fatal flaw?"
|
||||||
|
- "What is the 'Villain' (opposing force or misconception) we are fighting against?"
|
||||||
|
- "What is the emotional climax you want the viewer to feel at the end?"
|
||||||
|
|
||||||
|
${existingContext}
|
||||||
|
Output Language: ${input.language}.`,
|
||||||
|
|
||||||
|
temperature: 0.9,
|
||||||
|
|
||||||
|
schema: '{ "questions": ["Question 1", "Question 2", "Question 3", "Question 4"] }',
|
||||||
|
};
|
||||||
|
}
|
||||||
54
src/modules/skriptai/prompts/index.ts
Normal file
54
src/modules/skriptai/prompts/index.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* SkriptAI Prompt Index
|
||||||
|
*
|
||||||
|
* Centralized exports for all AI prompt builders.
|
||||||
|
* Each prompt is a pure function that takes typed input and returns
|
||||||
|
* { prompt, temperature, schema } — ready to pass to GeminiService methods.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Discovery & Research
|
||||||
|
export {
|
||||||
|
buildDiscoveryQuestionsPrompt,
|
||||||
|
type DiscoveryQuestionsInput,
|
||||||
|
} from './discovery-questions.prompt';
|
||||||
|
|
||||||
|
export {
|
||||||
|
buildSearchQueryPrompt,
|
||||||
|
buildSourceSearchPrompt,
|
||||||
|
type SearchQueryInput,
|
||||||
|
type SourceSearchInput,
|
||||||
|
} from './deep-research.prompt';
|
||||||
|
|
||||||
|
// Characters & Logline
|
||||||
|
export {
|
||||||
|
buildCharacterGenerationPrompt,
|
||||||
|
type CharacterGenerationInput,
|
||||||
|
} from './character-generation.prompt';
|
||||||
|
|
||||||
|
export { buildLoglinePrompt, type LoglineInput } from './logline.prompt';
|
||||||
|
|
||||||
|
// Script Generation
|
||||||
|
export {
|
||||||
|
buildScriptOutlinePrompt,
|
||||||
|
buildChapterSegmentPrompt,
|
||||||
|
buildSegmentRewritePrompt,
|
||||||
|
buildSegmentImagePrompt,
|
||||||
|
calculateTargetWordCount,
|
||||||
|
calculateEstimatedChapters,
|
||||||
|
type ScriptOutlineInput,
|
||||||
|
type ChapterSegmentInput,
|
||||||
|
type SegmentRewriteInput,
|
||||||
|
type SegmentImagePromptInput,
|
||||||
|
} from './script-generation.prompt';
|
||||||
|
|
||||||
|
// Analysis
|
||||||
|
export {
|
||||||
|
buildNeuroAnalysisPrompt,
|
||||||
|
buildYoutubeAuditPrompt,
|
||||||
|
buildCommercialBriefPrompt,
|
||||||
|
buildVisualAssetKeywordsPrompt,
|
||||||
|
type NeuroAnalysisInput,
|
||||||
|
type YoutubeAuditInput,
|
||||||
|
type CommercialBriefInput,
|
||||||
|
type VisualAssetKeywordsInput,
|
||||||
|
} from './analysis.prompt';
|
||||||
30
src/modules/skriptai/prompts/logline.prompt.ts
Normal file
30
src/modules/skriptai/prompts/logline.prompt.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Logline & High Concept Prompt Builder
|
||||||
|
*
|
||||||
|
* Uses Hollywood Producer persona with Dallas Jones formula
|
||||||
|
* to create compelling loglines and high concept premises.
|
||||||
|
*
|
||||||
|
* Used in: ResearchService.generateLogline()
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface LoglineInput {
|
||||||
|
topic: string;
|
||||||
|
sourceContext: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildLoglinePrompt(input: LoglineInput): {
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
schema: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
prompt: `Act as a Hollywood Producer. Topic: ${input.topic}. Material: ${input.sourceContext}.
|
||||||
|
Create a "High Concept" premise and a "Logline" (Max 25 words, Dallas Jones formula).
|
||||||
|
Language: ${input.language}.`,
|
||||||
|
|
||||||
|
temperature: 0.9,
|
||||||
|
|
||||||
|
schema: '{ "logline": "...", "highConcept": "..." }',
|
||||||
|
};
|
||||||
|
}
|
||||||
191
src/modules/skriptai/prompts/script-generation.prompt.ts
Normal file
191
src/modules/skriptai/prompts/script-generation.prompt.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/**
|
||||||
|
* Script Generation Prompt Builders
|
||||||
|
*
|
||||||
|
* Two-phase script generation:
|
||||||
|
* - Phase 1: Content outline (chapters, SEO, thumbnails)
|
||||||
|
* - Phase 2: Per-chapter segment generation
|
||||||
|
* - Segment rewrite with style change
|
||||||
|
* - Segment image prompt generation
|
||||||
|
*
|
||||||
|
* Used in: ScriptsService.generateScript(), rewriteSegment(), generateSegmentImage()
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Phase 1: Outline
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface ScriptOutlineInput {
|
||||||
|
topic: string;
|
||||||
|
logline: string;
|
||||||
|
characterContext: string;
|
||||||
|
speechStyles: string[];
|
||||||
|
targetAudience: string[];
|
||||||
|
contentType: string;
|
||||||
|
targetDuration: string;
|
||||||
|
targetWordCount: number;
|
||||||
|
estimatedChapters: number;
|
||||||
|
sourceContext: string;
|
||||||
|
briefContext: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildScriptOutlinePrompt(input: ScriptOutlineInput): {
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
schema: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
prompt: `Create a CONTENT OUTLINE.
|
||||||
|
Topic: "${input.topic}"
|
||||||
|
Logline: "${input.logline}"
|
||||||
|
Characters: ${input.characterContext}
|
||||||
|
Styles: ${input.speechStyles.join(', ')}. Audience: ${input.targetAudience.join(', ')}.
|
||||||
|
Format: ${input.contentType}. Target Duration: ${input.targetDuration}. Target Total Word Count: ${input.targetWordCount}.
|
||||||
|
Generate exactly ${input.estimatedChapters} chapters.
|
||||||
|
Material: ${input.sourceContext.substring(0, 15000)}
|
||||||
|
Brief: ${input.briefContext}`,
|
||||||
|
|
||||||
|
temperature: 0.7,
|
||||||
|
|
||||||
|
schema: `{
|
||||||
|
"title": "Title",
|
||||||
|
"seoDescription": "Desc",
|
||||||
|
"tags": ["tag1"],
|
||||||
|
"thumbnailIdeas": ["Idea 1"],
|
||||||
|
"chapters": [{ "title": "Chap 1", "focus": "Summary", "type": "Intro" }]
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Phase 2: Chapter → Segments
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface ChapterSegmentInput {
|
||||||
|
chapterIndex: number;
|
||||||
|
totalChapters: number;
|
||||||
|
chapterTitle: string;
|
||||||
|
chapterFocus: string;
|
||||||
|
chapterType: string;
|
||||||
|
speechStyles: string[];
|
||||||
|
targetAudience: string[];
|
||||||
|
characterContext: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildChapterSegmentPrompt(input: ChapterSegmentInput): {
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
schema: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
prompt: `Write Script Segment ${input.chapterIndex + 1}/${input.totalChapters}.
|
||||||
|
Chapter: "${input.chapterTitle}". Focus: ${input.chapterFocus}.
|
||||||
|
Style: ${input.speechStyles.join(', ')}.
|
||||||
|
Audience: ${input.targetAudience.join(', ')}.
|
||||||
|
Characters: ${input.characterContext}.
|
||||||
|
Target Length: ~200 words.
|
||||||
|
Language: ${input.language}.`,
|
||||||
|
|
||||||
|
temperature: 0.8,
|
||||||
|
|
||||||
|
schema: `[{
|
||||||
|
"segmentType": "${input.chapterType || 'Body'}",
|
||||||
|
"narratorScript": "Full text...",
|
||||||
|
"visualDescription": "Detailed visual explanation...",
|
||||||
|
"videoPrompt": "Cinematic shot of [subject], 4k...",
|
||||||
|
"imagePrompt": "Hyper-realistic photo of [subject]...",
|
||||||
|
"onScreenText": "Overlay text...",
|
||||||
|
"stockQuery": "Pexels keyword",
|
||||||
|
"audioCues": "SFX..."
|
||||||
|
}]`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Segment Rewrite
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface SegmentRewriteInput {
|
||||||
|
currentScript: string;
|
||||||
|
newStyle: string;
|
||||||
|
topic: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSegmentRewritePrompt(input: SegmentRewriteInput): {
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
schema: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
prompt: `Rewrite this script segment.
|
||||||
|
Current Text: "${input.currentScript}"
|
||||||
|
Goal: Change style to "${input.newStyle}".
|
||||||
|
Context: Topic is "${input.topic}". Language: ${input.language}.
|
||||||
|
Principles: Show Don't Tell, Subtext.`,
|
||||||
|
|
||||||
|
temperature: 0.85,
|
||||||
|
|
||||||
|
schema: `{
|
||||||
|
"narratorScript": "New text...",
|
||||||
|
"visualDescription": "Updated visual...",
|
||||||
|
"onScreenText": "Updated overlay...",
|
||||||
|
"audioCues": "Updated audio..."
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Segment Image Prompt
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface SegmentImagePromptInput {
|
||||||
|
topic: string;
|
||||||
|
narratorScript: string;
|
||||||
|
visualDescription: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSegmentImagePrompt(input: SegmentImagePromptInput): {
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
schema: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
prompt: `Create a detailed AI Image Generation Prompt and a Video Generation Prompt for this script segment.
|
||||||
|
Topic: "${input.topic}"
|
||||||
|
Segment Content: "${input.narratorScript}"
|
||||||
|
Visual Context: "${input.visualDescription}"
|
||||||
|
|
||||||
|
Goal: Create a highly detailed, cinematic, and artistic prompt optimized for tools like Midjourney, Flux, or Runway.
|
||||||
|
Style: Cinematic, highly detailed, 8k, professional lighting.`,
|
||||||
|
|
||||||
|
temperature: 0.7,
|
||||||
|
|
||||||
|
schema: `{
|
||||||
|
"imagePrompt": "Full detailed image prompt...",
|
||||||
|
"videoPrompt": "Full detailed video prompt..."
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Helpers
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate target word count based on duration string
|
||||||
|
*/
|
||||||
|
export function calculateTargetWordCount(targetDuration: string): number {
|
||||||
|
if (targetDuration.includes('Short')) return 140;
|
||||||
|
if (targetDuration.includes('Standard')) return 840;
|
||||||
|
if (targetDuration.includes('Long')) return 1680;
|
||||||
|
if (targetDuration.includes('Deep Dive')) return 2800;
|
||||||
|
return 840;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate estimated chapters based on word count
|
||||||
|
*/
|
||||||
|
export function calculateEstimatedChapters(targetWordCount: number): number {
|
||||||
|
return Math.ceil(targetWordCount / 200);
|
||||||
|
}
|
||||||
@@ -2,6 +2,12 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|||||||
import { PrismaService } from '../../../database/prisma.service';
|
import { PrismaService } from '../../../database/prisma.service';
|
||||||
import { GeminiService } from '../../gemini/gemini.service';
|
import { GeminiService } from '../../gemini/gemini.service';
|
||||||
import { NeuroAnalysisResult, YoutubeAudit } from '../types/skriptai.types';
|
import { NeuroAnalysisResult, YoutubeAudit } from '../types/skriptai.types';
|
||||||
|
import {
|
||||||
|
buildNeuroAnalysisPrompt,
|
||||||
|
buildYoutubeAuditPrompt,
|
||||||
|
buildCommercialBriefPrompt,
|
||||||
|
buildVisualAssetKeywordsPrompt,
|
||||||
|
} from '../prompts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AnalysisService
|
* AnalysisService
|
||||||
@@ -43,56 +49,12 @@ export class AnalysisService {
|
|||||||
.map((s) => s.narratorScript)
|
.map((s) => s.narratorScript)
|
||||||
.join('\n\n');
|
.join('\n\n');
|
||||||
|
|
||||||
const prompt = `Analyze this script using Consumer Neuroscience and Cialdini's 6 Principles of Persuasion.
|
const promptData = buildNeuroAnalysisPrompt({ fullScript });
|
||||||
|
|
||||||
Script:
|
|
||||||
${fullScript.substring(0, 10000)}
|
|
||||||
|
|
||||||
Provide:
|
|
||||||
1. Engagement Score (0-100): How well does it capture attention?
|
|
||||||
2. Dopamine Score (0-100): Does it create anticipation & reward loops?
|
|
||||||
3. Clarity Score (0-100): Is the message clear and memorable?
|
|
||||||
|
|
||||||
4. Cialdini's Persuasion Metrics (0-100 each):
|
|
||||||
- Reciprocity: Does it give value first?
|
|
||||||
- Scarcity: Does it create urgency?
|
|
||||||
- Authority: Does it establish credibility?
|
|
||||||
- Consistency: Does it align with viewer beliefs?
|
|
||||||
- Liking: Is the tone likeable/relatable?
|
|
||||||
- Social Proof: Does it reference others' actions?
|
|
||||||
|
|
||||||
5. Neuro Metrics:
|
|
||||||
- Attention Hooks: Moments that grab attention
|
|
||||||
- Emotional Triggers: Points that evoke emotion
|
|
||||||
- Memory Anchors: Unique/memorable elements
|
|
||||||
- Action Drivers: CTAs or challenges
|
|
||||||
|
|
||||||
6. Suggestions: 3-5 specific improvements
|
|
||||||
|
|
||||||
Return JSON: {
|
|
||||||
"engagementScore": number,
|
|
||||||
"dopamineScore": number,
|
|
||||||
"clarityScore": number,
|
|
||||||
"persuasionMetrics": {
|
|
||||||
"reciprocity": number,
|
|
||||||
"scarcity": number,
|
|
||||||
"authority": number,
|
|
||||||
"consistency": number,
|
|
||||||
"liking": number,
|
|
||||||
"socialProof": number
|
|
||||||
},
|
|
||||||
"neuroMetrics": {
|
|
||||||
"attentionHooks": ["..."],
|
|
||||||
"emotionalTriggers": ["..."],
|
|
||||||
"memoryAnchors": ["..."],
|
|
||||||
"actionDrivers": ["..."]
|
|
||||||
},
|
|
||||||
"suggestions": ["..."]
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<NeuroAnalysisResult>(
|
const resp = await this.gemini.generateJSON<NeuroAnalysisResult>(
|
||||||
prompt,
|
promptData.prompt,
|
||||||
'{ engagementScore, dopamineScore, clarityScore, persuasionMetrics, neuroMetrics, suggestions }',
|
promptData.schema,
|
||||||
|
{ temperature: promptData.temperature },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Save to project
|
// Save to project
|
||||||
@@ -124,52 +86,15 @@ Return JSON: {
|
|||||||
.map((s) => s.narratorScript)
|
.map((s) => s.narratorScript)
|
||||||
.join('\n\n');
|
.join('\n\n');
|
||||||
|
|
||||||
const prompt = `Perform a YouTube Algorithm Audit on this script for topic "${project.topic}".
|
const promptData = buildYoutubeAuditPrompt({
|
||||||
|
topic: project.topic,
|
||||||
Script:
|
fullScript,
|
||||||
${fullScript.substring(0, 10000)}
|
});
|
||||||
|
|
||||||
Analyze and provide:
|
|
||||||
1. Hook Score (0-100): First 10 seconds effectiveness
|
|
||||||
2. Pacing Score (0-100): Does it maintain momentum?
|
|
||||||
3. Viral Potential (0-100): Shareability factor
|
|
||||||
|
|
||||||
4. Retention Analysis: 3-5 potential drop-off points with time, issue, suggestion, severity (High/Medium/Low)
|
|
||||||
|
|
||||||
5. Thumbnail Concepts: 3 high-CTR thumbnail ideas with:
|
|
||||||
- Concept name
|
|
||||||
- Visual description
|
|
||||||
- Text overlay
|
|
||||||
- Color psychology
|
|
||||||
- Emotion target
|
|
||||||
- AI generation prompt
|
|
||||||
|
|
||||||
6. Title Options: 5 clickable titles (curiosity gap, numbers, power words)
|
|
||||||
|
|
||||||
7. Community Post: Engaging post to tease the video
|
|
||||||
|
|
||||||
8. Pinned Comment: Engagement-driving first comment
|
|
||||||
|
|
||||||
9. SEO Description: Optimized video description with keywords
|
|
||||||
|
|
||||||
10. Keywords: 10 relevant search keywords
|
|
||||||
|
|
||||||
Return JSON: {
|
|
||||||
"hookScore": number,
|
|
||||||
"pacingScore": number,
|
|
||||||
"viralPotential": number,
|
|
||||||
"retentionAnalysis": [{ "time": "0:30", "issue": "...", "suggestion": "...", "severity": "High" }],
|
|
||||||
"thumbnails": [{ "conceptName": "...", "visualDescription": "...", "textOverlay": "...", "colorPsychology": "...", "emotionTarget": "...", "aiPrompt": "..." }],
|
|
||||||
"titles": ["..."],
|
|
||||||
"communityPost": "...",
|
|
||||||
"pinnedComment": "...",
|
|
||||||
"description": "...",
|
|
||||||
"keywords": ["..."]
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<YoutubeAudit>(
|
const resp = await this.gemini.generateJSON<YoutubeAudit>(
|
||||||
prompt,
|
promptData.prompt,
|
||||||
'{ hookScore, pacingScore, viralPotential, retentionAnalysis, thumbnails, titles, communityPost, pinnedComment, description, keywords }',
|
promptData.schema,
|
||||||
|
{ temperature: promptData.temperature },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Save to project
|
// Save to project
|
||||||
@@ -201,37 +126,12 @@ Return JSON: {
|
|||||||
.map((s) => s.narratorScript)
|
.map((s) => s.narratorScript)
|
||||||
.join('\n\n');
|
.join('\n\n');
|
||||||
|
|
||||||
const prompt = `Analyze this content for commercial viability and sponsorship opportunities.
|
const promptData = buildCommercialBriefPrompt({
|
||||||
|
topic: project.topic,
|
||||||
Topic: "${project.topic}"
|
targetAudience: project.targetAudience,
|
||||||
Audience: ${project.targetAudience.join(', ')}
|
contentType: project.contentType,
|
||||||
Content Type: ${project.contentType}
|
fullScript,
|
||||||
|
});
|
||||||
Script excerpt:
|
|
||||||
${fullScript.substring(0, 5000)}
|
|
||||||
|
|
||||||
Provide:
|
|
||||||
1. Viability Score (1-10 scale as string): "8/10"
|
|
||||||
2. Viability Reason: Why this content is commercially viable
|
|
||||||
|
|
||||||
3. Sponsor Suggestions (3-5 potential sponsors):
|
|
||||||
- Company name
|
|
||||||
- Industry
|
|
||||||
- Match reason (why this sponsor fits)
|
|
||||||
- Email draft (outreach template)
|
|
||||||
|
|
||||||
Return JSON: {
|
|
||||||
"viabilityScore": "8/10",
|
|
||||||
"viabilityReason": "...",
|
|
||||||
"sponsors": [
|
|
||||||
{
|
|
||||||
"name": "Company Name",
|
|
||||||
"industry": "Tech/Finance/etc",
|
|
||||||
"matchReason": "...",
|
|
||||||
"emailDraft": "..."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<{
|
const resp = await this.gemini.generateJSON<{
|
||||||
viabilityScore: string;
|
viabilityScore: string;
|
||||||
@@ -242,7 +142,9 @@ Return JSON: {
|
|||||||
matchReason: string;
|
matchReason: string;
|
||||||
emailDraft: string;
|
emailDraft: string;
|
||||||
}[];
|
}[];
|
||||||
}>(prompt, '{ viabilityScore, viabilityReason, sponsors }');
|
}>(promptData.prompt, promptData.schema, {
|
||||||
|
temperature: promptData.temperature,
|
||||||
|
});
|
||||||
|
|
||||||
// Save to project
|
// Save to project
|
||||||
await this.prisma.scriptProject.update({
|
await this.prisma.scriptProject.update({
|
||||||
@@ -253,12 +155,6 @@ Return JSON: {
|
|||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate thumbnails using external image service
|
|
||||||
*
|
|
||||||
* @param prompt - Image generation prompt
|
|
||||||
* @returns Generated image URL
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* Generate thumbnails using external image service
|
* Generate thumbnails using external image service
|
||||||
* Applies "Nano Banana" prompt enrichment for high-quality results.
|
* Applies "Nano Banana" prompt enrichment for high-quality results.
|
||||||
@@ -302,13 +198,15 @@ Return JSON: {
|
|||||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt = `Generate ${count} specific, simple visual keywords for an image generator about "${project.topic}".
|
const promptData = buildVisualAssetKeywordsPrompt({
|
||||||
Format: "subject action context style". Keep it English, concise, no special chars.
|
topic: project.topic,
|
||||||
Return JSON array of strings.`;
|
count,
|
||||||
|
});
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<string[]>(
|
const resp = await this.gemini.generateJSON<string[]>(
|
||||||
prompt,
|
promptData.prompt,
|
||||||
'["keyword1", "keyword2", ...]',
|
promptData.schema,
|
||||||
|
{ temperature: promptData.temperature },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate image URLs and save to database
|
// Generate image URLs and save to database
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ import {
|
|||||||
CreateCharacterDto,
|
CreateCharacterDto,
|
||||||
} from '../dto';
|
} from '../dto';
|
||||||
import { CharacterRole } from '../types/skriptai.types';
|
import { CharacterRole } from '../types/skriptai.types';
|
||||||
|
import {
|
||||||
|
buildDiscoveryQuestionsPrompt,
|
||||||
|
buildSearchQueryPrompt,
|
||||||
|
buildSourceSearchPrompt,
|
||||||
|
buildCharacterGenerationPrompt,
|
||||||
|
buildLoglinePrompt,
|
||||||
|
} from '../prompts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ResearchService
|
* ResearchService
|
||||||
@@ -85,12 +92,8 @@ export class ResearchService {
|
|||||||
: project.topic;
|
: project.topic;
|
||||||
|
|
||||||
// Generate search queries
|
// Generate search queries
|
||||||
const queryPrompt = `Generate 5 specific Google Search queries for "${topic}".
|
|
||||||
Context: ${briefContext}. Language: ${project.language}.
|
|
||||||
Return strictly a JSON array of strings.`;
|
|
||||||
|
|
||||||
let searchQueries: string[] = [];
|
let searchQueries: string[] = [];
|
||||||
// Check if Gemini is available for queries
|
|
||||||
if (!this.gemini.isAvailable()) {
|
if (!this.gemini.isAvailable()) {
|
||||||
this.logger.warn('Gemini is disabled. Using mock search queries.');
|
this.logger.warn('Gemini is disabled. Using mock search queries.');
|
||||||
searchQueries = [
|
searchQueries = [
|
||||||
@@ -100,10 +103,19 @@ export class ResearchService {
|
|||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
const queryPromptData = buildSearchQueryPrompt({
|
||||||
|
topic,
|
||||||
|
briefContext,
|
||||||
|
language: project.language,
|
||||||
|
});
|
||||||
|
|
||||||
const queryResp = await this.gemini.generateJSON<string[]>(
|
const queryResp = await this.gemini.generateJSON<string[]>(
|
||||||
queryPrompt,
|
queryPromptData.prompt,
|
||||||
'["query1", "query2", ...]',
|
queryPromptData.schema,
|
||||||
{ tools: [{ googleSearch: {} }] },
|
{
|
||||||
|
temperature: queryPromptData.temperature,
|
||||||
|
tools: [{ googleSearch: {} }],
|
||||||
|
},
|
||||||
);
|
);
|
||||||
searchQueries = queryResp.data;
|
searchQueries = queryResp.data;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -130,12 +142,15 @@ export class ResearchService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourcePrompt = `Find 3 high-quality web sources for: ${query}. Language: ${project.language}.
|
const sourcePromptData = buildSourceSearchPrompt({
|
||||||
Return JSON array: [{ "title": string, "url": string, "snippet": string, "type": "article" }]`;
|
query,
|
||||||
|
language: project.language,
|
||||||
|
});
|
||||||
|
|
||||||
const sourceResp = await this.gemini.generateJSON<
|
const sourceResp = await this.gemini.generateJSON<
|
||||||
{ title: string; url: string; snippet: string; type: string }[]
|
{ title: string; url: string; snippet: string; type: string }[]
|
||||||
>(sourcePrompt, '[{ title, url, snippet, type }]', {
|
>(sourcePromptData.prompt, sourcePromptData.schema, {
|
||||||
|
temperature: sourcePromptData.temperature,
|
||||||
tools: [{ googleSearch: {} }],
|
tools: [{ googleSearch: {} }],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -221,11 +236,6 @@ export class ResearchService {
|
|||||||
language: string,
|
language: string,
|
||||||
existingQuestions: string[] = [],
|
existingQuestions: string[] = [],
|
||||||
) {
|
) {
|
||||||
const existingContext =
|
|
||||||
existingQuestions.length > 0
|
|
||||||
? `Avoid these questions: ${existingQuestions.join(', ')}`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
// Check if Gemini is available
|
// Check if Gemini is available
|
||||||
if (!this.gemini.isAvailable()) {
|
if (!this.gemini.isAvailable()) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
@@ -239,30 +249,16 @@ export class ResearchService {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt = `You are an expert Screenwriter and Creative Director. Topic: "${topic}".
|
const promptData = buildDiscoveryQuestionsPrompt({
|
||||||
|
topic,
|
||||||
PHASE 1: DEEP DIVE
|
language,
|
||||||
Think like a filmmaker. We are not just making a video; we are telling a story.
|
existingQuestions,
|
||||||
Analyze the topic "${topic}" to find the drama, the conflict, and the human element.
|
});
|
||||||
|
|
||||||
PHASE 2: INTERROGATION
|
|
||||||
Ask 3-4 provocative, "Screenwriter's Room" style questions to help shape the narrative arc.
|
|
||||||
|
|
||||||
DO NOT ASK: "What is the goal?" or "Who is the audience?".
|
|
||||||
|
|
||||||
INSTEAD ASK (Examples):
|
|
||||||
- "What is the 'Inciting Incident' that makes this topic urgent right now?"
|
|
||||||
- "If this topic was a character, what would be its fatal flaw?"
|
|
||||||
- "What is the 'Villain' (opposing force or misconception) we are fighting against?"
|
|
||||||
- "What is the emotional climax you want the viewer to feel at the end?"
|
|
||||||
|
|
||||||
${existingContext}
|
|
||||||
Output Language: ${language}.
|
|
||||||
Return JSON object: { "questions": ["Question 1", "Question 2", "Question 3", "Question 4"] }`;
|
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<{ questions: string[] }>(
|
const resp = await this.gemini.generateJSON<{ questions: string[] }>(
|
||||||
prompt,
|
promptData.prompt,
|
||||||
'{ questions: string[] }',
|
promptData.schema,
|
||||||
|
{ temperature: promptData.temperature },
|
||||||
);
|
);
|
||||||
|
|
||||||
return resp.data.questions;
|
return resp.data.questions;
|
||||||
@@ -312,15 +308,11 @@ export class ResearchService {
|
|||||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt = `Create Character Profiles for a ${project.contentType} about "${project.topic}".
|
const promptData = buildCharacterGenerationPrompt({
|
||||||
Use Alan C. Hueth's "Triunity of Character" model:
|
contentType: project.contentType,
|
||||||
1. Values (Inner belief)
|
topic: project.topic,
|
||||||
2. Traits (Personality)
|
language: project.language,
|
||||||
3. Mannerisms (External behavior)
|
});
|
||||||
|
|
||||||
If format is non-fiction (Youtube Doc), create a 'Host/Narrator' persona and potentially an 'Antagonist' (e.g., The Problem, Time, A Rival).
|
|
||||||
Language: ${project.language}.
|
|
||||||
Return JSON Array: [{ "name": "Name", "role": "Protagonist", "values": "...", "traits": "...", "mannerisms": "..." }]`;
|
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<
|
const resp = await this.gemini.generateJSON<
|
||||||
{
|
{
|
||||||
@@ -330,7 +322,9 @@ export class ResearchService {
|
|||||||
traits: string;
|
traits: string;
|
||||||
mannerisms: string;
|
mannerisms: string;
|
||||||
}[]
|
}[]
|
||||||
>(prompt, '[{ name, role, values, traits, mannerisms }]');
|
>(promptData.prompt, promptData.schema, {
|
||||||
|
temperature: promptData.temperature,
|
||||||
|
});
|
||||||
|
|
||||||
// Save characters to database
|
// Save characters to database
|
||||||
const characters = await Promise.all(
|
const characters = await Promise.all(
|
||||||
@@ -371,15 +365,18 @@ export class ResearchService {
|
|||||||
|
|
||||||
const sourceContext = project.sources.map((s) => s.snippet).join('\n');
|
const sourceContext = project.sources.map((s) => s.snippet).join('\n');
|
||||||
|
|
||||||
const prompt = `Act as a Hollywood Producer. Topic: ${project.topic}. Material: ${sourceContext}.
|
const promptData = buildLoglinePrompt({
|
||||||
Create a "High Concept" premise and a "Logline" (Max 25 words, Dallas Jones formula).
|
topic: project.topic,
|
||||||
Language: ${project.language}.
|
sourceContext,
|
||||||
Return JSON: { "logline": "...", "highConcept": "..." }`;
|
language: project.language,
|
||||||
|
});
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<{
|
const resp = await this.gemini.generateJSON<{
|
||||||
logline: string;
|
logline: string;
|
||||||
highConcept: string;
|
highConcept: string;
|
||||||
}>(prompt, '{ logline, highConcept }');
|
}>(promptData.prompt, promptData.schema, {
|
||||||
|
temperature: promptData.temperature,
|
||||||
|
});
|
||||||
|
|
||||||
// Update project
|
// Update project
|
||||||
await this.prisma.scriptProject.update({
|
await this.prisma.scriptProject.update({
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|||||||
import { PrismaService } from '../../../database/prisma.service';
|
import { PrismaService } from '../../../database/prisma.service';
|
||||||
import { GeminiService } from '../../gemini/gemini.service';
|
import { GeminiService } from '../../gemini/gemini.service';
|
||||||
import { CreateSegmentDto, UpdateSegmentDto } from '../dto';
|
import { CreateSegmentDto, UpdateSegmentDto } from '../dto';
|
||||||
|
|
||||||
import { AnalysisService } from './analysis.service';
|
import { AnalysisService } from './analysis.service';
|
||||||
|
import {
|
||||||
// AI_CONFIG is only used for model selection reference
|
buildScriptOutlinePrompt,
|
||||||
|
buildChapterSegmentPrompt,
|
||||||
|
buildSegmentRewritePrompt,
|
||||||
|
buildSegmentImagePrompt,
|
||||||
|
calculateTargetWordCount,
|
||||||
|
calculateEstimatedChapters,
|
||||||
|
} from '../prompts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ScriptsService
|
* ScriptsService
|
||||||
@@ -138,34 +143,24 @@ export class ScriptsService {
|
|||||||
)
|
)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
// Calculate target word count based on duration
|
// Calculate target metrics
|
||||||
let targetWordCount = 840;
|
const targetWordCount = calculateTargetWordCount(project.targetDuration);
|
||||||
if (project.targetDuration.includes('Short')) targetWordCount = 140;
|
const estimatedChapters = calculateEstimatedChapters(targetWordCount);
|
||||||
else if (project.targetDuration.includes('Standard')) targetWordCount = 840;
|
|
||||||
else if (project.targetDuration.includes('Long')) targetWordCount = 1680;
|
|
||||||
else if (project.targetDuration.includes('Deep Dive'))
|
|
||||||
targetWordCount = 2800;
|
|
||||||
|
|
||||||
const estimatedChapters = Math.ceil(targetWordCount / 200);
|
// PHASE 1: Generate Outline using prompt builder
|
||||||
|
const outlinePromptData = buildScriptOutlinePrompt({
|
||||||
// PHASE 1: Generate Outline
|
topic: project.topic,
|
||||||
const outlinePrompt = `
|
logline: project.logline || '',
|
||||||
Create a CONTENT OUTLINE.
|
characterContext,
|
||||||
Topic: "${project.topic}"
|
speechStyles: project.speechStyle,
|
||||||
Logline: "${project.logline || ''}"
|
targetAudience: project.targetAudience,
|
||||||
Characters: ${characterContext}
|
contentType: project.contentType,
|
||||||
Styles: ${project.speechStyle.join(', ')}. Audience: ${project.targetAudience.join(', ')}.
|
targetDuration: project.targetDuration,
|
||||||
Format: ${project.contentType}. Target Duration: ${project.targetDuration}. Target Total Word Count: ${targetWordCount}.
|
targetWordCount,
|
||||||
Generate exactly ${estimatedChapters} chapters.
|
estimatedChapters,
|
||||||
Material: ${sourceContext.substring(0, 15000)}
|
sourceContext,
|
||||||
Brief: ${briefContext}
|
briefContext,
|
||||||
|
});
|
||||||
Return JSON: {
|
|
||||||
"title": "Title", "seoDescription": "Desc", "tags": ["tag1"],
|
|
||||||
"thumbnailIdeas": ["Idea 1"],
|
|
||||||
"chapters": [{ "title": "Chap 1", "focus": "Summary", "type": "Intro" }]
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const outlineResp = await this.gemini.generateJSON<{
|
const outlineResp = await this.gemini.generateJSON<{
|
||||||
title: string;
|
title: string;
|
||||||
@@ -173,10 +168,9 @@ export class ScriptsService {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
thumbnailIdeas: string[];
|
thumbnailIdeas: string[];
|
||||||
chapters: { title: string; focus: string; type: string }[];
|
chapters: { title: string; focus: string; type: string }[];
|
||||||
}>(
|
}>(outlinePromptData.prompt, outlinePromptData.schema, {
|
||||||
outlinePrompt,
|
temperature: outlinePromptData.temperature,
|
||||||
'{ title, seoDescription, tags, thumbnailIdeas, chapters }',
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const outlineData = outlineResp.data;
|
const outlineData = outlineResp.data;
|
||||||
|
|
||||||
@@ -191,38 +185,30 @@ export class ScriptsService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// PHASE 2: Generate each chapter
|
// PHASE 2: Generate each chapter using prompt builder
|
||||||
const generatedSegments: any[] = [];
|
const generatedSegments: any[] = [];
|
||||||
let timeOffset = 0;
|
let timeOffset = 0;
|
||||||
|
|
||||||
for (let i = 0; i < outlineData.chapters.length; i++) {
|
for (let i = 0; i < outlineData.chapters.length; i++) {
|
||||||
const chapter = outlineData.chapters[i];
|
const chapter = outlineData.chapters[i];
|
||||||
|
|
||||||
const chapterPrompt = `
|
const chapterPromptData = buildChapterSegmentPrompt({
|
||||||
Write Script Segment ${i + 1}/${outlineData.chapters.length}.
|
chapterIndex: i,
|
||||||
Chapter: "${chapter.title}". Focus: ${chapter.focus}.
|
totalChapters: outlineData.chapters.length,
|
||||||
Style: ${project.speechStyle.join(', ')}.
|
chapterTitle: chapter.title,
|
||||||
Audience: ${project.targetAudience.join(', ')}.
|
chapterFocus: chapter.focus,
|
||||||
Characters: ${characterContext}.
|
chapterType: chapter.type,
|
||||||
Target Length: ~200 words.
|
speechStyles: project.speechStyle,
|
||||||
Language: ${project.language}.
|
targetAudience: project.targetAudience,
|
||||||
|
characterContext,
|
||||||
Return JSON Array: [{
|
language: project.language,
|
||||||
"segmentType": "${chapter.type || 'Body'}",
|
});
|
||||||
"narratorScript": "Full text...",
|
|
||||||
"visualDescription": "Detailed visual explanation...",
|
|
||||||
"videoPrompt": "Cinematic shot of [subject], 4k...",
|
|
||||||
"imagePrompt": "Hyper-realistic photo of [subject]...",
|
|
||||||
"onScreenText": "Overlay text...",
|
|
||||||
"stockQuery": "Pexels keyword",
|
|
||||||
"audioCues": "SFX..."
|
|
||||||
}]
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const segmentResp = await this.gemini.generateJSON<any[]>(
|
const segmentResp = await this.gemini.generateJSON<any[]>(
|
||||||
chapterPrompt,
|
chapterPromptData.prompt,
|
||||||
'[{ segmentType, narratorScript, visualDescription, videoPrompt, imagePrompt, onScreenText, stockQuery, audioCues }]',
|
chapterPromptData.schema,
|
||||||
|
{ temperature: chapterPromptData.temperature },
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const seg of segmentResp.data) {
|
for (const seg of segmentResp.data) {
|
||||||
@@ -293,30 +279,21 @@ export class ScriptsService {
|
|||||||
throw new NotFoundException(`Segment with ID ${segmentId} not found`);
|
throw new NotFoundException(`Segment with ID ${segmentId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt = `
|
const promptData = buildSegmentRewritePrompt({
|
||||||
Rewrite this script segment.
|
currentScript: segment.narratorScript || '',
|
||||||
Current Text: "${segment.narratorScript}"
|
newStyle,
|
||||||
Goal: Change style to "${newStyle}".
|
topic: segment.project.topic,
|
||||||
Context: Topic is "${segment.project.topic}". Language: ${segment.project.language}.
|
language: segment.project.language,
|
||||||
Principles: Show Don't Tell, Subtext.
|
});
|
||||||
|
|
||||||
Return JSON: {
|
|
||||||
"narratorScript": "New text...",
|
|
||||||
"visualDescription": "Updated visual...",
|
|
||||||
"onScreenText": "Updated overlay...",
|
|
||||||
"audioCues": "Updated audio..."
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const rewriteResp = await this.gemini.generateJSON<{
|
const rewriteResp = await this.gemini.generateJSON<{
|
||||||
narratorScript: string;
|
narratorScript: string;
|
||||||
visualDescription: string;
|
visualDescription: string;
|
||||||
onScreenText: string;
|
onScreenText: string;
|
||||||
audioCues: string;
|
audioCues: string;
|
||||||
}>(
|
}>(promptData.prompt, promptData.schema, {
|
||||||
prompt,
|
temperature: promptData.temperature,
|
||||||
'{ narratorScript, visualDescription, onScreenText, audioCues }',
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const data = rewriteResp.data;
|
const data = rewriteResp.data;
|
||||||
const words = data.narratorScript
|
const words = data.narratorScript
|
||||||
@@ -359,25 +336,18 @@ export class ScriptsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. Generate/Refine Image Prompt using LLM
|
// 1. Generate/Refine Image Prompt using LLM
|
||||||
const promptGenPrompt = `
|
const promptData = buildSegmentImagePrompt({
|
||||||
Create a detailed AI Image Generation Prompt and a Video Generation Prompt for this script segment.
|
topic: segment.project.topic,
|
||||||
Topic: "${segment.project.topic}"
|
narratorScript: segment.narratorScript || '',
|
||||||
Segment Content: "${segment.narratorScript}"
|
visualDescription: segment.visualDescription || '',
|
||||||
Visual Context: "${segment.visualDescription}"
|
});
|
||||||
|
|
||||||
Goal: Create a highly detailed, cinematic, and artistic prompt optimized for tools like Midjourney, Flux, or Runway.
|
|
||||||
Style: Cinematic, highly detailed, 8k, professional lighting.
|
|
||||||
|
|
||||||
Return JSON: {
|
|
||||||
"imagePrompt": "Full detailed image prompt...",
|
|
||||||
"videoPrompt": "Full detailed video prompt..."
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const prompts = await this.gemini.generateJSON<{
|
const prompts = await this.gemini.generateJSON<{
|
||||||
imagePrompt: string;
|
imagePrompt: string;
|
||||||
videoPrompt: string;
|
videoPrompt: string;
|
||||||
}>(promptGenPrompt, '{ imagePrompt, videoPrompt }');
|
}>(promptData.prompt, promptData.schema, {
|
||||||
|
temperature: promptData.temperature,
|
||||||
|
});
|
||||||
|
|
||||||
// 2. Use the new image prompt for generation
|
// 2. Use the new image prompt for generation
|
||||||
const imageUrl = await this.analysisService.generateThumbnailImage(
|
const imageUrl = await this.analysisService.generateThumbnailImage(
|
||||||
|
|||||||
Reference in New Issue
Block a user