generated from fahricansecer/boilerplate-be
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e8c9d17d0 |
@@ -1,2 +0,0 @@
|
|||||||
# Auto detect text files and perform LF normalization
|
|
||||||
* text=auto
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
name: Backend Deploy 🚀
|
||||||
|
run-name: ${{ gitea.actor }} backend güncelliyor...
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Kodu Çek
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Docker Build
|
||||||
|
# Dockerfile'ı kullanarak imajı oluşturuyoruz
|
||||||
|
run: docker build -t backend-proje:latest .
|
||||||
|
|
||||||
|
- name: Eski Konteyneri Sil
|
||||||
|
# İlk seferde hata vermemesi için '|| true' ekledik
|
||||||
|
run: docker rm -f backend-container || true
|
||||||
|
|
||||||
|
- name: Yeni Versiyonu Başlat
|
||||||
|
# Secrets kısmından şifreleri alıp konteynere veriyoruz
|
||||||
|
run: |
|
||||||
|
docker run -d \
|
||||||
|
--name backend-container \
|
||||||
|
--restart always \
|
||||||
|
--network gitea-server_gitea \
|
||||||
|
-p 1501:3000 \
|
||||||
|
-e DATABASE_URL='${{ secrets.DATABASE_URL }}' \
|
||||||
|
-e JWT_SECRET='${{ secrets.JWT_SECRET }}' \
|
||||||
|
-e REDIS_HOST='${{ secrets.REDIS_HOST }}' \
|
||||||
|
-e REDIS_PORT='6379' \
|
||||||
|
backend-proje:latest
|
||||||
+26
-19
@@ -1,49 +1,56 @@
|
|||||||
# Build stage
|
# --- Build Stage ---
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package files
|
# Raspberry Pi ve Prisma uyumluluğu için gerekli kütüphaneler
|
||||||
COPY package*.json ./
|
RUN apk add --no-cache openssl libc6-compat
|
||||||
|
|
||||||
# Install dependencies
|
# Paket dosyalarını kopyala
|
||||||
|
COPY package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
# Copy source code
|
# Kaynak kodları kopyala
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Generate Prisma client
|
# Prisma client üret (Database şeman için şart)
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
|
|
||||||
# Build the application
|
# Build al (NestJS/Backend için)
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Production stage
|
# --- Production Stage (Canlı Sistem) ---
|
||||||
FROM node:20-alpine AS production
|
FROM node:20-alpine AS production
|
||||||
|
|
||||||
|
# Prisma için gerekli kütüphaneleri buraya da ekliyoruz
|
||||||
|
RUN apk add --no-cache openssl libc6-compat
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package files
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install production dependencies only
|
# Sadece production (canlıda lazım olan) paketleri kur
|
||||||
RUN npm ci --only=production
|
RUN npm ci --only=production
|
||||||
|
|
||||||
# Copy Prisma schema and generate client
|
# Prisma şemasını taşı ve client üret
|
||||||
COPY prisma ./prisma
|
COPY prisma ./prisma
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
|
|
||||||
# Copy built application
|
# Build edilen dosyaları taşı (Senin Dockerfile'ındaki yapıya sadık kaldım)
|
||||||
COPY --from=builder /app/dist ./dist
|
# Güvenlik için dosyaları 'node' kullanıcısına zimmetliyoruz
|
||||||
|
COPY --chown=node:node --from=builder /app/dist ./dist
|
||||||
|
|
||||||
# Copy i18n files
|
# Eğer i18n varsa onu da taşı
|
||||||
COPY --from=builder /app/src/i18n ./dist/i18n
|
COPY --chown=node:node --from=builder /app/src/i18n ./dist/i18n
|
||||||
|
|
||||||
# Set environment
|
# Ortam değişkeni
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
# Expose port
|
# Portu aç
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# Start the application
|
# Güvenlik: Root yerine 'node' kullanıcısına geç
|
||||||
CMD ["node", "dist/main.js"]
|
USER node
|
||||||
|
|
||||||
|
# Uygulamayı başlat
|
||||||
|
CMD ["node", "dist/main.js"]
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
apps: [{
|
|
||||||
name: 'skriptai-backend',
|
|
||||||
script: 'dist/main.js',
|
|
||||||
instances: 1,
|
|
||||||
exec_mode: 'cluster',
|
|
||||||
env_production: {
|
|
||||||
NODE_ENV: 'production',
|
|
||||||
PORT: 3000
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
name: 'skriptai-frontend',
|
|
||||||
script: 'npm',
|
|
||||||
args: 'start -- -p 3001',
|
|
||||||
cwd: '/home/haruncan/apps/skriptai/frontend',
|
|
||||||
env: {
|
|
||||||
NODE_ENV: 'production',
|
|
||||||
PORT: 3001
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
|
||||||
|
```markdown
|
||||||
|
# 🚀 Raspberry Pi Proje Dağıtım Rehberi (SSL Güncellemeli)
|
||||||
|
|
||||||
|
Bu rehber, mevcut merkezi altyapıyı (Gitea, Runner, Central Database, Nginx) kullanarak yeni NestJS (Backend) ve Next.js (Frontend) projelerini nasıl ayağa kaldıracağını ve **SSL (HTTPS) sorunları yaşamadan** nasıl yayına alacağını adım adım açıklar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗 1. Altyapı Hazırlığı (Infrastructure)
|
||||||
|
|
||||||
|
### A. Veritabanı Oluşturma
|
||||||
|
Her yeni proje için merkezi PostgreSQL konteynerinde yeni bir veritabanı açılmalıdır.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PROJE_ADI kısmını küçük harf ve boşluksuz yaz (örn: e_ticaret_db)
|
||||||
|
docker exec -it backend_db createdb -U 'Rub1c0N-UseR.!' PROJE_ADI_db
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. DNS Ayarları
|
||||||
|
Domain panelinden (Cloudflare vb.) yeni subdomain'leri Raspberry Pi'nin dış IP'sine yönlendir:
|
||||||
|
* `api-proje.bilgich.com` -> **Raspberry Pi IP**
|
||||||
|
* `ui-proje.bilgich.com` -> **Raspberry Pi IP**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 2. Gitea Secret Ayarları
|
||||||
|
|
||||||
|
**⚠️ Önemli:** Proje canlıya çıkarken SSL kullanacağımız için URL'leri şimdiden **https** olarak tanımlıyoruz.
|
||||||
|
|
||||||
|
Gitea reponuzda **Settings > Actions > Secrets** yolunu izleyerek aşağıdaki anahtarları tanımlayın.
|
||||||
|
|
||||||
|
### Backend İçin:
|
||||||
|
|
||||||
|
| Key | Value Örneği | Açıklama |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `DATABASE_URL` | `postgresql://Rub1c0N-UseR.%21:SIFRE%3D@backend_db:5432/PROJE_ADI_db?schema=public` | Özel karakterler encode edilmeli (!=%21, =%3D) |
|
||||||
|
| `JWT_SECRET` | `rastgele_uzun_string` | Güvenlik anahtarı |
|
||||||
|
|
||||||
|
### Frontend İçin:
|
||||||
|
|
||||||
|
| Key | Value Örneği | Açıklama |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `NEXT_PUBLIC_API_URL` | `https://api-proje.bilgich.com/api` | **https** olmasına dikkat et |
|
||||||
|
| `NEXTAUTH_URL` | `https://ui-proje.bilgich.com` | **https** olmasına dikkat et |
|
||||||
|
| `NEXTAUTH_SECRET` | `openssl_rand_base64_32_cikti` | Auth güvenliği için |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 3. Backend (NestJS) Proje Ayarları
|
||||||
|
|
||||||
|
1. **main.ts:** Global prefix ve Swagger yollarını kontrol et.
|
||||||
|
2. **Dockerfile:** Mevcut çalışan NestJS Dockerfile'ı kullan.
|
||||||
|
3. **deploy.yml Değişiklikleri:**
|
||||||
|
* `--name backend-PROJE-container` (Her proje için unique isim)
|
||||||
|
* `-p 150X:3000` (Sıradaki boş port: 1502, 1503...)
|
||||||
|
* `--network gitea-server_gitea` (Database'e erişim için şart)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 4. Frontend (Next.js) Proje Ayarları
|
||||||
|
|
||||||
|
1. **next.config.js:** `output: 'standalone'` satırını ekle.
|
||||||
|
2. **Dockerfile:** `ARG` ve `ENV` satırlarına `NEXT_PUBLIC_` değişkenlerini ekle.
|
||||||
|
3. **deploy-ui.yml Değişiklikleri:**
|
||||||
|
* `--build-arg` ile tüm Gitea secret'larını build aşamasına geç.
|
||||||
|
* `--name ui-PROJE-container` (Unique isim)
|
||||||
|
* `-p 180X:3000` (Sıradaki boş port: 1801, 1802...)
|
||||||
|
* `-e NEXTAUTH_URL` ve `NEXTAUTH_SECRET` runtime değişkenlerini ekle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚦 5. Nginx ve SSL Yönlendirmesi (KRİTİK ADIM)
|
||||||
|
|
||||||
|
Diğer projelerle çakışma olmaması için her yeni domain'e mutlaka SSL kurulmalıdır.
|
||||||
|
|
||||||
|
### A. Konfigürasyon Dosyası Oluştur
|
||||||
|
Backend veya Frontend için dosya oluşturun:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/nginx/sites-available/proje-api
|
||||||
|
# Veya
|
||||||
|
sudo nano /etc/nginx/sites-available/proje-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. İçeriği Yapıştır (Sadece HTTP)
|
||||||
|
İlk aşamada sadece 80 portunu dinleyen şu bloğu yapıştırın:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name api-proje.bilgich.com; # Domain adını buraya yaz
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:150X; # Uygulamanın dış portu (1502, 1802 vb.)
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
|
# Gerçek IP Logları
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### C. Nginx'i Aktif Et
|
||||||
|
Dosyayı `sites-enabled` klasörüne linkleyin ve Nginx'i reload edin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ln -s /etc/nginx/sites-available/proje-api /etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### D. SSL Sertifikası Kur (Certbot)
|
||||||
|
Bu komut Nginx ayarlarını otomatik güncelleyip 443 portunu açacaktır. Bu adımı yapmazsanız HTTPS girişleri başka projelere yönlenir!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo certbot --nginx -d api-proje.bilgich.com
|
||||||
|
```
|
||||||
|
*Soru sorarsa "2" (Redirect) seçeneğini seçin.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 "Senior" İpuçları & Bakım
|
||||||
|
|
||||||
|
### Port Yönetimi (Defteri Kebir)
|
||||||
|
Kullandığın portları çakışmaması için mutlaka not et:
|
||||||
|
* **1501:** Digicraft BE
|
||||||
|
* **1502:** YeniProje BE
|
||||||
|
* **1800:** Digicraft UI
|
||||||
|
* **1801:** YeniProje UI
|
||||||
|
|
||||||
|
### Disk Temizliği
|
||||||
|
Raspberry Pi diski dolmaması için haftalık/aylık temizlik yap:
|
||||||
|
```bash
|
||||||
|
docker system prune -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Takibi
|
||||||
|
Bir sorun olduğunda ilk buraya bak:
|
||||||
|
```bash
|
||||||
|
docker logs -f KONTEYNER_ADI
|
||||||
|
```
|
||||||
|
```
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Learned Protocols & Standards
|
||||||
|
|
||||||
|
This document serves as the persistent memory of the protocols, standards, and personas learned from the `skript-be` and `skript-ui` repositories.
|
||||||
|
|
||||||
|
## 1. Frontend Standards (skript-ui)
|
||||||
|
|
||||||
|
### Design & Aesthetics (`frontend-design`)
|
||||||
|
- **Anti-AI Slop:** Avoid generic, cookie-cutter "AI" aesthetics (e.g., standard purple gradients, predictable layouts).
|
||||||
|
- **Boldness:** Commit to a specific aesthetic direction (Minimalist, Brutalist, Magazine, etc.).
|
||||||
|
- **Typography:** Use distinctive fonts; avoid system defaults like Arial/Inter unless intentional.
|
||||||
|
- **Micro-interactions:** Prioritize one or two high-impact animations over scattered noise.
|
||||||
|
- **Creativity:** Use noise textures, gradient meshes, asymmetry, and overlapping elements.
|
||||||
|
|
||||||
|
### Architecture (`senior-frontend` & `nextjs-architecture-expert`)
|
||||||
|
- **Next.js App Router:** STRICT adherence to App Router patterns (layouts, error.tsx, loading.tsx).
|
||||||
|
- **Server Components (RSC):** Default to Server Components. Use Client Components ('use client') only when interactivity is required.
|
||||||
|
- **State Management:** component-first thinking; use Context/Zustand for global state, local state for UI.
|
||||||
|
- **Performance:** Aim for sub-3s load times. Use `next/image`, code splitting, and lazy loading.
|
||||||
|
- **Tailwind CSS:** Use correctly; avoid long string pollution where possible (use utils/cva).
|
||||||
|
|
||||||
|
### Quality Assurance (`senior-qa`)
|
||||||
|
- **E2E Testing:** Critical flows must be tested.
|
||||||
|
- **Coverage:** High unit test coverage for utilities and complex logic.
|
||||||
|
|
||||||
|
## 2. Backend Standards (skript-be)
|
||||||
|
|
||||||
|
### Code Quality (`code-reviewer`)
|
||||||
|
- **Review:** Verify BEFORE implementing.
|
||||||
|
- **Simplicity:** No over-engineering.
|
||||||
|
- **Security:** No secrets in code. Input validation is mandatory.
|
||||||
|
- **YAGNI:** "You Aren't Gonna Need It" - don't build features "just in case".
|
||||||
|
|
||||||
|
### Security (`security-engineer` & `api-security-audit`)
|
||||||
|
- **Zero Trust:** Verify every request.
|
||||||
|
- **OWASP:** Check against Top 10 (Injection, Broken Auth, etc.).
|
||||||
|
- **Data:** Validate all inputs using libraries (e.g., Zod, Joi).
|
||||||
|
- **Logging:** Sanitize logs (no PII/secrets).
|
||||||
|
|
||||||
|
### Database (`database-optimizer`)
|
||||||
|
- **N+1:** Watch out for N+1 queries in loops/ORMs.
|
||||||
|
- **Indexing:** Index foreign keys and search columns.
|
||||||
|
- **Explain:** Check execution plans for complex queries.
|
||||||
|
|
||||||
|
### General Engineering
|
||||||
|
- **TypeScript:** Strict mode enabled. No `any`. Use generics and utility types (`typescript-pro`).
|
||||||
|
- **Feedback:** "Receive Code Review" protocol – technical correctness > polite agreement. Verify suggestions before applying.
|
||||||
|
|
||||||
|
### TypeScript Expertise (`typescript-pro`)
|
||||||
|
- **Seniority:** I write *Senior-level* code. This means focusing on maintainability, scalability, and robustness, not just "making it work".
|
||||||
|
- **Modern Techniques:** I utilize the latest TypeScript features:
|
||||||
|
- **Advanced Types:** Conditional types, Template Literal Types, Mapped Types.
|
||||||
|
- **Utility Types:** `Pick`, `Omit`, `Partial`, `Readonly`, `ReturnType`, `Parameters`, etc.
|
||||||
|
- **Generics:** Proper constraints (`T extends ...`) and defaults.
|
||||||
|
- **Type Inference:** Leveraging inference where clean, explicit typing where necessary for clarity.
|
||||||
|
- **Strictness:**
|
||||||
|
- `noImplicitAny` is law.
|
||||||
|
- Avoid `any` at all costs; use `unknown` with type narrowing/guards if dynamic typing is truly needed.
|
||||||
|
- Strict null checks always on.
|
||||||
|
- **Architecture:** Value objects, opaque types, and branded types for domain safety.
|
||||||
|
|
||||||
|
## 3. Operational Protocols
|
||||||
|
|
||||||
|
- **Agent Persona:** I act as the specific specialist required for the task (e.g., if debugging, I am `debugger`; if designing, I am `frontend-developer`).
|
||||||
|
- **Proactiveness:** I do not wait for permission to fix obvious bugs or improve clear performace bottlenecks if they are within scope.
|
||||||
|
- **Persistence:** These rules apply to ALL future tasks in this session.
|
||||||
Generated
+40
-20
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "skriptAI-be",
|
"name": "bbb",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "skriptAI-be",
|
"name": "bbb",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -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.6"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
@@ -1137,6 +1137,7 @@
|
|||||||
"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",
|
||||||
@@ -3074,6 +3075,7 @@
|
|||||||
"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",
|
||||||
@@ -3240,6 +3242,7 @@
|
|||||||
"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",
|
||||||
@@ -3285,6 +3288,7 @@
|
|||||||
"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",
|
||||||
@@ -3364,6 +3368,7 @@
|
|||||||
"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",
|
||||||
@@ -3384,6 +3389,7 @@
|
|||||||
"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"
|
||||||
@@ -3718,6 +3724,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -3782,6 +3789,7 @@
|
|||||||
"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",
|
||||||
@@ -4687,6 +4695,7 @@
|
|||||||
"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": "*"
|
||||||
@@ -4801,6 +4810,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -4965,6 +4975,7 @@
|
|||||||
"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",
|
||||||
@@ -5602,6 +5613,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -5655,6 +5667,7 @@
|
|||||||
"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",
|
||||||
@@ -6144,6 +6157,7 @@
|
|||||||
"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",
|
||||||
@@ -6217,6 +6231,7 @@
|
|||||||
"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",
|
||||||
@@ -6290,6 +6305,7 @@
|
|||||||
"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"
|
||||||
@@ -6441,6 +6457,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -6484,12 +6501,14 @@
|
|||||||
"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",
|
||||||
@@ -7175,8 +7194,7 @@
|
|||||||
"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",
|
||||||
@@ -7235,6 +7253,7 @@
|
|||||||
"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",
|
||||||
@@ -7294,6 +7313,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -8654,6 +8674,7 @@
|
|||||||
"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",
|
||||||
@@ -9497,6 +9518,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -10241,7 +10263,6 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -10425,6 +10446,7 @@
|
|||||||
"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",
|
||||||
@@ -10551,6 +10573,7 @@
|
|||||||
"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",
|
||||||
@@ -10580,6 +10603,7 @@
|
|||||||
"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",
|
||||||
@@ -10733,6 +10757,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -10786,6 +10811,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -11850,6 +11876,7 @@
|
|||||||
"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",
|
||||||
@@ -12164,6 +12191,7 @@
|
|||||||
"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",
|
||||||
@@ -12301,6 +12329,7 @@
|
|||||||
"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"
|
||||||
@@ -12639,7 +12668,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -12657,7 +12685,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -12670,7 +12697,6 @@
|
|||||||
"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"
|
||||||
@@ -12684,7 +12710,6 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -12693,15 +12718,13 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -12711,7 +12734,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -12724,7 +12746,6 @@
|
|||||||
"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",
|
||||||
@@ -12932,10 +12953,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.3.6",
|
"version": "4.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "skriptAI-be",
|
"name": "bbb",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "Generated by Antigravity CLI",
|
"description": "Generated by Antigravity CLI",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -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.6"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ model User {
|
|||||||
// Relations
|
// Relations
|
||||||
roles UserRole[]
|
roles UserRole[]
|
||||||
refreshTokens RefreshToken[]
|
refreshTokens RefreshToken[]
|
||||||
projects ScriptProject[]
|
|
||||||
|
|
||||||
// Multi-tenancy (optional)
|
// Multi-tenancy (optional)
|
||||||
tenantId String?
|
tenantId String?
|
||||||
@@ -161,193 +160,3 @@ model Translation {
|
|||||||
@@index([locale])
|
@@index([locale])
|
||||||
@@index([namespace])
|
@@index([namespace])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// SkriptAI Models
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
model ScriptProject {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
userId String?
|
|
||||||
topic String
|
|
||||||
contentType String // ContentFormat enum value
|
|
||||||
targetAudience String[] // Array of TargetAudience values
|
|
||||||
speechStyle String[] // Array of SpeechStyle values
|
|
||||||
targetDuration String
|
|
||||||
userNotes String? @db.Text
|
|
||||||
tone String?
|
|
||||||
language String @default("tr")
|
|
||||||
logline String? @db.Text
|
|
||||||
highConcept String? @db.Text
|
|
||||||
includeInterviews Boolean @default(false)
|
|
||||||
|
|
||||||
// Project Status
|
|
||||||
status String @default("DRAFT") // DRAFT, RESEARCHING, SCRIPTING, ANALYZING, COMPLETED
|
|
||||||
currentVersionNumber Int @default(0)
|
|
||||||
|
|
||||||
// SEO Data (stored as JSON)
|
|
||||||
seoTitle String?
|
|
||||||
seoDescription String? @db.Text
|
|
||||||
seoTags String[]
|
|
||||||
thumbnailIdeas String[]
|
|
||||||
|
|
||||||
// Analysis Results (stored as JSON)
|
|
||||||
neuroAnalysis Json?
|
|
||||||
youtubeAudit Json?
|
|
||||||
postProduction Json?
|
|
||||||
commercialBrief Json?
|
|
||||||
|
|
||||||
// Timestamps & Soft Delete
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime?
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
user User? @relation(fields: [userId], references: [id])
|
|
||||||
segments ScriptSegment[]
|
|
||||||
sources ResearchSource[]
|
|
||||||
characters CharacterProfile[]
|
|
||||||
briefItems BriefItem[]
|
|
||||||
visualAssets VisualAsset[]
|
|
||||||
versions ScriptVersion[]
|
|
||||||
|
|
||||||
@@index([userId])
|
|
||||||
@@index([topic])
|
|
||||||
@@index([status])
|
|
||||||
}
|
|
||||||
|
|
||||||
model ScriptSegment {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
projectId String
|
|
||||||
segmentType String // Hook, Intro, Body, Ad/Sponsor, CTA, Outro, Scene, Dialogue, Section, Headline
|
|
||||||
timeStart String
|
|
||||||
duration String
|
|
||||||
visualDescription String? @db.Text
|
|
||||||
narratorScript String? @db.Text
|
|
||||||
editorNotes String? @db.Text
|
|
||||||
generalNotes String? @db.Text
|
|
||||||
audioCues String?
|
|
||||||
onScreenText String?
|
|
||||||
stockQuery String?
|
|
||||||
videoPrompt String? @db.Text
|
|
||||||
imagePrompt String? @db.Text
|
|
||||||
citationIndexes Int[]
|
|
||||||
generatedImageUrl String?
|
|
||||||
sortOrder Int @default(0)
|
|
||||||
|
|
||||||
// Timestamps
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
project ScriptProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([projectId])
|
|
||||||
@@index([sortOrder])
|
|
||||||
}
|
|
||||||
|
|
||||||
model ResearchSource {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
projectId String
|
|
||||||
title String
|
|
||||||
url String
|
|
||||||
snippet String? @db.Text
|
|
||||||
type String // article, video, interview, academic, book, document
|
|
||||||
selected Boolean @default(true)
|
|
||||||
isNew Boolean @default(false)
|
|
||||||
|
|
||||||
// Timestamps
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
project ScriptProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([projectId])
|
|
||||||
}
|
|
||||||
|
|
||||||
model CharacterProfile {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
projectId String
|
|
||||||
name String
|
|
||||||
role String // Protagonist, Antagonist, Guide/Mentor, Sidekick, Narrator
|
|
||||||
values String? @db.Text
|
|
||||||
traits String? @db.Text
|
|
||||||
mannerisms String? @db.Text
|
|
||||||
|
|
||||||
// Timestamps
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
project ScriptProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([projectId])
|
|
||||||
}
|
|
||||||
|
|
||||||
model BriefItem {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
projectId String
|
|
||||||
question String @db.Text
|
|
||||||
answer String @db.Text
|
|
||||||
sortOrder Int @default(0)
|
|
||||||
|
|
||||||
// Timestamps
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
project ScriptProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([projectId])
|
|
||||||
}
|
|
||||||
|
|
||||||
model VisualAsset {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
projectId String
|
|
||||||
url String
|
|
||||||
desc String?
|
|
||||||
selected Boolean @default(true)
|
|
||||||
|
|
||||||
// Timestamps
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
project ScriptProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([projectId])
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// Version History
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
model ScriptVersion {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
projectId String
|
|
||||||
versionNumber Int
|
|
||||||
label String? // User-defined label, e.g. "Final Draft", "Before Rewrite"
|
|
||||||
generatedBy String @default("AI") // AI | USER | AUTO_SAVE
|
|
||||||
|
|
||||||
// Snapshot data: complete segments at this point in time
|
|
||||||
snapshotData Json // Array of segment objects
|
|
||||||
|
|
||||||
// Optional: SEO snapshot
|
|
||||||
seoSnapshot Json? // { seoTitle, seoDescription, seoTags, thumbnailIdeas }
|
|
||||||
|
|
||||||
// Metadata
|
|
||||||
segmentCount Int @default(0)
|
|
||||||
totalWords Int @default(0)
|
|
||||||
changeNote String? @db.Text // What changed in this version
|
|
||||||
|
|
||||||
// Timestamps
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
project ScriptProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@unique([projectId, versionNumber])
|
|
||||||
@@index([projectId])
|
|
||||||
@@index([createdAt])
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
upstream skriptai_backend {
|
|
||||||
server 127.0.0.1:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
upstream skriptai_frontend {
|
|
||||||
server 127.0.0.1:3001;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name skript.bilgich.com 192.168.1.199;
|
|
||||||
|
|
||||||
# Frontend (Root)
|
|
||||||
location / {
|
|
||||||
proxy_pass http://skriptai_frontend;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Backend API
|
|
||||||
location /api {
|
|
||||||
proxy_pass http://skriptai_backend;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
upstream skriptai_backend {
|
|
||||||
server 127.0.0.1:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
upstream skriptai_frontend {
|
|
||||||
server 127.0.0.1:3001;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
location / {
|
|
||||||
proxy_pass http://skriptai_frontend;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Backend API
|
|
||||||
location /api {
|
|
||||||
proxy_pass http://skriptai_backend;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
upstream skriptai_backend {
|
|
||||||
server 127.0.0.1:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name skript.bilgich.com 192.168.1.199;
|
|
||||||
|
|
||||||
root /var/www/skriptai;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api {
|
|
||||||
proxy_pass http://skriptai_backend;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -39,7 +39,6 @@ import { UsersModule } from './modules/users/users.module';
|
|||||||
import { AdminModule } from './modules/admin/admin.module';
|
import { AdminModule } from './modules/admin/admin.module';
|
||||||
import { HealthModule } from './modules/health/health.module';
|
import { HealthModule } from './modules/health/health.module';
|
||||||
import { GeminiModule } from './modules/gemini/gemini.module';
|
import { GeminiModule } from './modules/gemini/gemini.module';
|
||||||
import { SkriptaiModule } from './modules/skriptai/skriptai.module';
|
|
||||||
|
|
||||||
// Guards
|
// Guards
|
||||||
import {
|
import {
|
||||||
@@ -160,7 +159,6 @@ import {
|
|||||||
|
|
||||||
// Optional Modules (controlled by env variables)
|
// Optional Modules (controlled by env variables)
|
||||||
GeminiModule,
|
GeminiModule,
|
||||||
SkriptaiModule,
|
|
||||||
HealthModule,
|
HealthModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export class GlobalExceptionFilter implements ExceptionFilter {
|
|||||||
});
|
});
|
||||||
// Only update if translation exists (key is different from result)
|
// Only update if translation exists (key is different from result)
|
||||||
if (translatedMessage !== `errors.${message}`) {
|
if (translatedMessage !== `errors.${message}`) {
|
||||||
message = translatedMessage;
|
message = translatedMessage as string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const envSchema = z.object({
|
|||||||
|
|
||||||
// JWT
|
// JWT
|
||||||
JWT_SECRET: z.string().min(32),
|
JWT_SECRET: z.string().min(32),
|
||||||
JWT_ACCESS_EXPIRATION: z.string().default('24h'),
|
JWT_ACCESS_EXPIRATION: z.string().default('15m'),
|
||||||
JWT_REFRESH_EXPIRATION: z.string().default('7d'),
|
JWT_REFRESH_EXPIRATION: z.string().default('7d'),
|
||||||
|
|
||||||
// Redis
|
// Redis
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"PROJECT_CREATED": "Project created successfully",
|
|
||||||
"PROJECT_UPDATED": "Project updated",
|
|
||||||
"PROJECT_DELETED": "Project deleted",
|
|
||||||
"PROJECT_DUPLICATED": "Project duplicated",
|
|
||||||
"SCRIPT_GENERATED": "Script generated successfully",
|
|
||||||
"SCRIPT_REWRITTEN": "Segment rewritten",
|
|
||||||
"RESEARCH_COMPLETE": "Research completed",
|
|
||||||
"SOURCES_ADDED": "Sources added",
|
|
||||||
"BRIEF_UPDATED": "Brief updated",
|
|
||||||
"CHARACTERS_GENERATED": "Characters generated",
|
|
||||||
"LOGLINE_GENERATED": "Logline and high concept generated",
|
|
||||||
"NEURO_ANALYSIS_COMPLETE": "Neuro marketing analysis completed",
|
|
||||||
"YOUTUBE_AUDIT_COMPLETE": "YouTube audit completed",
|
|
||||||
"COMMERCIAL_BRIEF_READY": "Commercial brief ready",
|
|
||||||
"EXPORT_READY": "Export ready",
|
|
||||||
"VISUAL_ASSETS_GENERATED": "Visual assets generated"
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"PROJECT_CREATED": "Proje başarıyla oluşturuldu",
|
|
||||||
"PROJECT_UPDATED": "Proje güncellendi",
|
|
||||||
"PROJECT_DELETED": "Proje silindi",
|
|
||||||
"PROJECT_DUPLICATED": "Proje kopyalandı",
|
|
||||||
"SCRIPT_GENERATED": "Script başarıyla oluşturuldu",
|
|
||||||
"SCRIPT_REWRITTEN": "Segment yeniden yazıldı",
|
|
||||||
"RESEARCH_COMPLETE": "Araştırma tamamlandı",
|
|
||||||
"SOURCES_ADDED": "Kaynaklar eklendi",
|
|
||||||
"BRIEF_UPDATED": "Brief güncellendi",
|
|
||||||
"CHARACTERS_GENERATED": "Karakterler oluşturuldu",
|
|
||||||
"LOGLINE_GENERATED": "Logline ve high concept oluşturuldu",
|
|
||||||
"NEURO_ANALYSIS_COMPLETE": "Nöro pazarlama analizi tamamlandı",
|
|
||||||
"YOUTUBE_AUDIT_COMPLETE": "YouTube denetimi tamamlandı",
|
|
||||||
"COMMERCIAL_BRIEF_READY": "Ticari brief hazır",
|
|
||||||
"EXPORT_READY": "Dışa aktarım hazır",
|
|
||||||
"VISUAL_ASSETS_GENERATED": "Görsel varlıklar oluşturuldu"
|
|
||||||
}
|
|
||||||
+4
-1
@@ -18,7 +18,10 @@ async function bootstrap() {
|
|||||||
app.useGlobalInterceptors(new LoggerErrorInterceptor());
|
app.useGlobalInterceptors(new LoggerErrorInterceptor());
|
||||||
|
|
||||||
// Security Headers
|
// Security Headers
|
||||||
app.use(helmet());
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: false,
|
||||||
|
crossOriginEmbedderPolicy: false,
|
||||||
|
}));
|
||||||
|
|
||||||
// Graceful Shutdown (Prisma & Docker)
|
// Graceful Shutdown (Prisma & Docker)
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
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;
|
||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
tools?: any[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GeminiChatMessage {
|
export interface GeminiChatMessage {
|
||||||
@@ -20,72 +14,30 @@ 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error types for Gemini API failures
|
* Gemini AI Service
|
||||||
*/
|
|
||||||
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/JSON/image generation using Google Gemini API.
|
* Provides AI-powered text 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');
|
||||||
*
|
*
|
||||||
* // JSON generation with Zod validation
|
* // With options
|
||||||
* import { z } from 'zod';
|
* const response = await geminiService.generateText('Translate to Turkish', {
|
||||||
* const schema = z.object({ title: z.string(), score: z.number() });
|
* temperature: 0.7,
|
||||||
* const result = await geminiService.generateJSON(
|
* systemPrompt: 'You are a professional translator',
|
||||||
* '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()
|
||||||
@@ -134,10 +86,6 @@ 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
|
||||||
*
|
*
|
||||||
@@ -149,10 +97,11 @@ export class GeminiService implements OnModuleInit {
|
|||||||
prompt: string,
|
prompt: string,
|
||||||
options: GeminiGenerateOptions = {},
|
options: GeminiGenerateOptions = {},
|
||||||
): Promise<{ text: string; usage?: any }> {
|
): Promise<{ text: string; usage?: any }> {
|
||||||
this.ensureAvailable();
|
if (!this.isAvailable()) {
|
||||||
|
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[] = [];
|
||||||
@@ -180,31 +129,19 @@ export class GeminiService implements OnModuleInit {
|
|||||||
config: {
|
config: {
|
||||||
temperature: options.temperature,
|
temperature: options.temperature,
|
||||||
maxOutputTokens: options.maxTokens,
|
maxOutputTokens: options.maxTokens,
|
||||||
tools: options.tools,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
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) {
|
||||||
const durationMs = Date.now() - startTime;
|
this.logger.error('Gemini generation failed', error);
|
||||||
this.logger.error(
|
throw 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
|
||||||
*
|
*
|
||||||
@@ -216,10 +153,11 @@ export class GeminiService implements OnModuleInit {
|
|||||||
messages: GeminiChatMessage[],
|
messages: GeminiChatMessage[],
|
||||||
options: GeminiGenerateOptions = {},
|
options: GeminiGenerateOptions = {},
|
||||||
): Promise<{ text: string; usage?: any }> {
|
): Promise<{ text: string; usage?: any }> {
|
||||||
this.ensureAvailable();
|
if (!this.isAvailable()) {
|
||||||
|
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) => ({
|
||||||
@@ -250,406 +188,53 @@ 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 this.classifyError(error);
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// JSON Generation (Enhanced)
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate structured JSON output with retry, recovery, and optional Zod validation.
|
* Generate structured JSON output
|
||||||
*
|
|
||||||
* 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 (human readable)
|
* @param schema - JSON schema description for the expected output
|
||||||
* @param options - Optional configuration including zodSchema and maxRetries
|
* @param options - Optional configuration for the generation
|
||||||
* @returns Parsed and optionally validated JSON object
|
* @returns Parsed JSON object
|
||||||
*/
|
*/
|
||||||
async generateJSON<T = any>(
|
async generateJSON<T = any>(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
schema: string,
|
schema: string,
|
||||||
options: GeminiJSONOptions<T> = {},
|
options: GeminiGenerateOptions = {},
|
||||||
): Promise<{ data: T; usage?: any }> {
|
): Promise<{ data: T; usage?: any }> {
|
||||||
this.ensureAvailable();
|
const fullPrompt = `${prompt}
|
||||||
|
|
||||||
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}
|
|
||||||
|
|
||||||
Output the result as valid JSON that matches this schema:
|
Output the result as valid JSON that matches this schema:
|
||||||
${schema}
|
${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 contents: any[] = [];
|
const response = await this.generateText(fullPrompt, options);
|
||||||
|
|
||||||
if (options.systemPrompt) {
|
|
||||||
contents.push({
|
|
||||||
role: 'user',
|
|
||||||
parts: [{ text: options.systemPrompt }],
|
|
||||||
});
|
|
||||||
contents.push({
|
|
||||||
role: 'model',
|
|
||||||
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;
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
lastError = error as Error;
|
|
||||||
|
|
||||||
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)
|
|
||||||
*
|
|
||||||
* @param prompt - Image description
|
|
||||||
* @returns Base64 encoded image data URI
|
|
||||||
*/
|
|
||||||
async generateImage(prompt: string): Promise<string> {
|
|
||||||
this.ensureAvailable();
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use Imagen 3.0 (Nano Banana Pro)
|
// Try to extract JSON from the response
|
||||||
const model = 'imagen-3.0-generate-001';
|
let jsonStr = response.text;
|
||||||
|
|
||||||
const response = (await this.client!.models.generateImages({
|
// Remove potential markdown code blocks
|
||||||
model,
|
const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||||
prompt,
|
if (jsonMatch) {
|
||||||
config: {
|
jsonStr = jsonMatch[1].trim();
|
||||||
numberOfImages: 1,
|
|
||||||
aspectRatio: '16:9',
|
|
||||||
},
|
|
||||||
})) as any;
|
|
||||||
|
|
||||||
const durationMs = Date.now() - startTime;
|
|
||||||
this.logger.log(
|
|
||||||
`Image generated in ${durationMs}ms (model: ${model})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
response.images &&
|
|
||||||
response.images.length > 0 &&
|
|
||||||
response.images[0].image
|
|
||||||
) {
|
|
||||||
// Return as Data URI
|
|
||||||
return `data:image/png;base64,${response.images[0].image}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new GeminiException(
|
const data = JSON.parse(jsonStr) as T;
|
||||||
'No image returned from Gemini',
|
return { data, usage: response.usage };
|
||||||
GeminiErrorType.INVALID_RESPONSE,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof GeminiException) throw error;
|
this.logger.error('Failed to parse JSON response', error);
|
||||||
this.logger.error('Gemini image generation failed', error);
|
throw new Error('Failed to parse AI response as JSON');
|
||||||
throw this.classifyError(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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import {
|
|
||||||
Controller,
|
|
||||||
Post,
|
|
||||||
Body,
|
|
||||||
Param,
|
|
||||||
UseGuards,
|
|
||||||
Query,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import {
|
|
||||||
ApiTags,
|
|
||||||
ApiOperation,
|
|
||||||
ApiResponse,
|
|
||||||
ApiBearerAuth,
|
|
||||||
} from '@nestjs/swagger';
|
|
||||||
import { AnalysisService } from '../services';
|
|
||||||
import { JwtAuthGuard } from '../../auth/guards';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AnalysisController
|
|
||||||
*
|
|
||||||
* REST API controller for content analysis endpoints.
|
|
||||||
*
|
|
||||||
* TR: İçerik analizi endpoint'leri için REST API controller.
|
|
||||||
* EN: REST API controller for content analysis endpoints.
|
|
||||||
*/
|
|
||||||
@ApiTags('SkriptAI - Analysis')
|
|
||||||
@Controller('skriptai/analysis')
|
|
||||||
export class AnalysisController {
|
|
||||||
constructor(private readonly analysisService: AnalysisService) {}
|
|
||||||
|
|
||||||
@Post(':projectId/neuro')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Perform Neuro Marketing Analysis' })
|
|
||||||
@ApiResponse({ status: 201, description: 'Analysis completed' })
|
|
||||||
async analyzeNeuroMarketing(@Param('projectId') projectId: string) {
|
|
||||||
return this.analysisService.analyzeNeuroMarketing(projectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post(':projectId/youtube-audit')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Perform YouTube Algorithm Audit' })
|
|
||||||
@ApiResponse({ status: 201, description: 'Audit completed' })
|
|
||||||
async performYoutubeAudit(@Param('projectId') projectId: string) {
|
|
||||||
return this.analysisService.performYoutubeAudit(projectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post(':projectId/commercial-brief')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Generate Commercial Brief (Sponsorship Analysis)' })
|
|
||||||
@ApiResponse({ status: 201, description: 'Brief generated' })
|
|
||||||
async generateCommercialBrief(@Param('projectId') projectId: string) {
|
|
||||||
return this.analysisService.generateCommercialBrief(projectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post(':projectId/visual-assets')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Generate visual assets for project' })
|
|
||||||
async generateVisualAssets(
|
|
||||||
@Param('projectId') projectId: string,
|
|
||||||
@Query('count') count: number = 5,
|
|
||||||
) {
|
|
||||||
return this.analysisService.generateVisualAssets(projectId, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('thumbnail')
|
|
||||||
@ApiOperation({ summary: 'Generate a thumbnail image from prompt' })
|
|
||||||
async generateThumbnail(@Body() body: { prompt: string }) {
|
|
||||||
const url = await this.analysisService.generateThumbnailImage(body.prompt);
|
|
||||||
return { url };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export * from './projects.controller';
|
|
||||||
export * from './scripts.controller';
|
|
||||||
export * from './research.controller';
|
|
||||||
export * from './analysis.controller';
|
|
||||||
export * from './versions.controller';
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import {
|
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
Post,
|
|
||||||
Put,
|
|
||||||
Delete,
|
|
||||||
Body,
|
|
||||||
Param,
|
|
||||||
UseGuards,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import {
|
|
||||||
ApiTags,
|
|
||||||
ApiOperation,
|
|
||||||
ApiResponse,
|
|
||||||
ApiBearerAuth,
|
|
||||||
} from '@nestjs/swagger';
|
|
||||||
import { ProjectsService } from '../services';
|
|
||||||
import { CreateProjectDto, UpdateProjectDto } from '../dto';
|
|
||||||
import { JwtAuthGuard } from '../../auth/guards';
|
|
||||||
import { CurrentUser } from '../../../common/decorators';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ProjectsController
|
|
||||||
*
|
|
||||||
* REST API controller for script project management.
|
|
||||||
*
|
|
||||||
* TR: Script projesi yönetimi için REST API controller.
|
|
||||||
* EN: REST API controller for script project management.
|
|
||||||
*/
|
|
||||||
@ApiTags('SkriptAI - Projects')
|
|
||||||
@Controller('skriptai/projects')
|
|
||||||
export class ProjectsController {
|
|
||||||
constructor(private readonly projectsService: ProjectsService) {}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Create a new script project' })
|
|
||||||
@ApiResponse({ status: 201, description: 'Project created successfully' })
|
|
||||||
async create(@Body() createDto: CreateProjectDto, @CurrentUser() user: any) {
|
|
||||||
return this.projectsService.create(createDto, user?.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Get all projects for current user' })
|
|
||||||
async findAll(@CurrentUser() user: any) {
|
|
||||||
return this.projectsService.findAll(user?.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':id')
|
|
||||||
@ApiOperation({ summary: 'Get a project by ID' })
|
|
||||||
async findOne(@Param('id') id: string) {
|
|
||||||
return this.projectsService.findOne(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Put(':id')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Update a project' })
|
|
||||||
async update(@Param('id') id: string, @Body() updateDto: UpdateProjectDto) {
|
|
||||||
return this.projectsService.update(id, updateDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(':id')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Soft delete a project' })
|
|
||||||
async remove(@Param('id') id: string) {
|
|
||||||
return this.projectsService.remove(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post(':id/duplicate')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Duplicate a project with all content' })
|
|
||||||
async duplicate(@Param('id') id: string, @CurrentUser() user: any) {
|
|
||||||
return this.projectsService.duplicate(id, user?.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':id/export')
|
|
||||||
@ApiOperation({ summary: 'Export project to JSON format' })
|
|
||||||
async exportToJson(@Param('id') id: string) {
|
|
||||||
return this.projectsService.exportToJson(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
import {
|
|
||||||
Controller,
|
|
||||||
Post,
|
|
||||||
Put,
|
|
||||||
Delete,
|
|
||||||
Body,
|
|
||||||
Param,
|
|
||||||
UseGuards,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import {
|
|
||||||
ApiTags,
|
|
||||||
ApiOperation,
|
|
||||||
ApiResponse,
|
|
||||||
ApiBearerAuth,
|
|
||||||
} from '@nestjs/swagger';
|
|
||||||
import { ResearchService } from '../services';
|
|
||||||
import {
|
|
||||||
CreateSourceDto,
|
|
||||||
CreateBriefItemDto,
|
|
||||||
CreateCharacterDto,
|
|
||||||
PerformResearchDto,
|
|
||||||
GenerateDiscoveryQuestionsDto,
|
|
||||||
GenerateLoglineDto,
|
|
||||||
GenerateCharactersDto,
|
|
||||||
} from '../dto';
|
|
||||||
import { JwtAuthGuard } from '../../auth/guards';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ResearchController
|
|
||||||
*
|
|
||||||
* REST API controller for research sources, creative brief, and characters.
|
|
||||||
*
|
|
||||||
* TR: Araştırma kaynakları, yaratıcı brief ve karakterler için REST API controller.
|
|
||||||
* EN: REST API controller for research sources, creative brief, and characters.
|
|
||||||
*/
|
|
||||||
@ApiTags('SkriptAI - Research')
|
|
||||||
@Controller('skriptai/research')
|
|
||||||
export class ResearchController {
|
|
||||||
constructor(private readonly researchService: ResearchService) {}
|
|
||||||
|
|
||||||
// ========== SOURCES ==========
|
|
||||||
|
|
||||||
@Post('sources')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Add a research source' })
|
|
||||||
async addSource(@Body() createDto: CreateSourceDto) {
|
|
||||||
return this.researchService.addSource(createDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Put('sources/:id/toggle')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Toggle source selection' })
|
|
||||||
async toggleSource(@Param('id') id: string) {
|
|
||||||
return this.researchService.toggleSourceSelection(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete('sources/:id')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Delete a research source' })
|
|
||||||
async deleteSource(@Param('id') id: string) {
|
|
||||||
return this.researchService.deleteSource(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('deep-research')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Perform deep research using AI' })
|
|
||||||
@ApiResponse({ status: 201, description: 'Research completed' })
|
|
||||||
async performDeepResearch(@Body() researchDto: PerformResearchDto) {
|
|
||||||
return this.researchService.performDeepResearch(
|
|
||||||
researchDto.projectId,
|
|
||||||
researchDto.additionalQuery,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== BRIEF ITEMS ==========
|
|
||||||
|
|
||||||
@Post('brief-items')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Add a brief item (Q&A)' })
|
|
||||||
async addBriefItem(@Body() createDto: CreateBriefItemDto) {
|
|
||||||
return this.researchService.addBriefItem(createDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Put('brief-items/:id')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Update a brief item answer' })
|
|
||||||
async updateBriefItem(
|
|
||||||
@Param('id') id: string,
|
|
||||||
@Body() body: { answer: string },
|
|
||||||
) {
|
|
||||||
return this.researchService.updateBriefItem(id, body.answer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete('brief-items/:id')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Delete a brief item' })
|
|
||||||
async deleteBriefItem(@Param('id') id: string) {
|
|
||||||
return this.researchService.deleteBriefItem(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('discovery-questions')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Generate discovery questions using AI' })
|
|
||||||
async generateDiscoveryQuestions(@Body() dto: GenerateDiscoveryQuestionsDto) {
|
|
||||||
return this.researchService.generateDiscoveryQuestions(
|
|
||||||
dto.topic,
|
|
||||||
dto.language,
|
|
||||||
dto.existingQuestions,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== CHARACTERS ==========
|
|
||||||
|
|
||||||
@Post('characters')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Add a character profile' })
|
|
||||||
async addCharacter(@Body() createDto: CreateCharacterDto) {
|
|
||||||
return this.researchService.addCharacter(createDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Put('characters/:id')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Update a character profile' })
|
|
||||||
async updateCharacter(
|
|
||||||
@Param('id') id: string,
|
|
||||||
@Body() updateDto: Partial<CreateCharacterDto>,
|
|
||||||
) {
|
|
||||||
return this.researchService.updateCharacter(id, updateDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete('characters/:id')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Delete a character profile' })
|
|
||||||
async deleteCharacter(@Param('id') id: string) {
|
|
||||||
return this.researchService.deleteCharacter(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('characters/generate')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Auto-generate character profiles using AI' })
|
|
||||||
async generateCharacters(@Body() dto: GenerateCharactersDto) {
|
|
||||||
return this.researchService.generateCharacters(dto.projectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== LOGLINE ==========
|
|
||||||
|
|
||||||
@Post('logline')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Generate logline and high concept' })
|
|
||||||
async generateLogline(@Body() dto: GenerateLoglineDto) {
|
|
||||||
return this.researchService.generateLogline(dto.projectId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import {
|
|
||||||
Controller,
|
|
||||||
Post,
|
|
||||||
Put,
|
|
||||||
Delete,
|
|
||||||
Body,
|
|
||||||
Param,
|
|
||||||
UseGuards,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import {
|
|
||||||
ApiTags,
|
|
||||||
ApiOperation,
|
|
||||||
ApiResponse,
|
|
||||||
ApiBearerAuth,
|
|
||||||
} from '@nestjs/swagger';
|
|
||||||
import { ScriptsService } from '../services';
|
|
||||||
import {
|
|
||||||
CreateSegmentDto,
|
|
||||||
UpdateSegmentDto,
|
|
||||||
RewriteSegmentDto,
|
|
||||||
GenerateScriptDto,
|
|
||||||
} from '../dto';
|
|
||||||
import { JwtAuthGuard } from '../../auth/guards';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ScriptsController
|
|
||||||
*
|
|
||||||
* REST API controller for script segments and AI generation.
|
|
||||||
*
|
|
||||||
* TR: Script segmentleri ve AI üretimi için REST API controller.
|
|
||||||
* EN: REST API controller for script segments and AI generation.
|
|
||||||
*/
|
|
||||||
@ApiTags('SkriptAI - Scripts')
|
|
||||||
@Controller('skriptai/scripts')
|
|
||||||
export class ScriptsController {
|
|
||||||
constructor(private readonly scriptsService: ScriptsService) {}
|
|
||||||
|
|
||||||
@Post('segments')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Create a new script segment' })
|
|
||||||
async createSegment(@Body() createDto: CreateSegmentDto) {
|
|
||||||
return this.scriptsService.createSegment(createDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Put('segments/:id')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Update a script segment' })
|
|
||||||
async updateSegment(
|
|
||||||
@Param('id') id: string,
|
|
||||||
@Body() updateDto: UpdateSegmentDto,
|
|
||||||
) {
|
|
||||||
return this.scriptsService.updateSegment(id, updateDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete('segments/:id')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Delete a script segment' })
|
|
||||||
async deleteSegment(@Param('id') id: string) {
|
|
||||||
return this.scriptsService.deleteSegment(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('segments/reorder')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Reorder segments in a project' })
|
|
||||||
async reorderSegments(
|
|
||||||
@Body() body: { projectId: string; segmentIds: string[] },
|
|
||||||
) {
|
|
||||||
return this.scriptsService.reorderSegments(body.projectId, body.segmentIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('generate')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Generate full script using AI' })
|
|
||||||
@ApiResponse({ status: 201, description: 'Script generated successfully' })
|
|
||||||
async generateScript(@Body() generateDto: GenerateScriptDto) {
|
|
||||||
return this.scriptsService.generateScript(generateDto.projectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('rewrite')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Rewrite a segment with new style' })
|
|
||||||
async rewriteSegment(@Body() rewriteDto: RewriteSegmentDto) {
|
|
||||||
return this.scriptsService.rewriteSegment(
|
|
||||||
rewriteDto.segmentId,
|
|
||||||
rewriteDto.newStyle,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('segments/:id/image')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Generate image for a segment' })
|
|
||||||
async generateSegmentImage(@Param('id') id: string) {
|
|
||||||
return this.scriptsService.generateSegmentImage(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('segments/:id/regenerate')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Regenerate a single segment with AI' })
|
|
||||||
async regenerateSegment(@Param('id') id: string) {
|
|
||||||
return this.scriptsService.regenerateSegment(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('regenerate-partial')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@ApiOperation({ summary: 'Regenerate selected segments with AI' })
|
|
||||||
async regeneratePartial(
|
|
||||||
@Body() body: { projectId: string; segmentIds: string[] },
|
|
||||||
) {
|
|
||||||
return this.scriptsService.regeneratePartial(
|
|
||||||
body.projectId,
|
|
||||||
body.segmentIds,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import {
|
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
Post,
|
|
||||||
Delete,
|
|
||||||
Param,
|
|
||||||
Body,
|
|
||||||
Query,
|
|
||||||
Logger,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth } from '@nestjs/swagger';
|
|
||||||
import { VersionsService } from '../services/versions.service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VersionsController
|
|
||||||
*
|
|
||||||
* REST API for managing script version history.
|
|
||||||
*
|
|
||||||
* Endpoints:
|
|
||||||
* - GET /projects/:projectId/versions — List all versions
|
|
||||||
* - GET /projects/:projectId/versions/:id — Get version details
|
|
||||||
* - POST /projects/:projectId/versions — Manual save (create snapshot)
|
|
||||||
* - POST /projects/:projectId/versions/:id/restore — Restore to version
|
|
||||||
* - DELETE /projects/:projectId/versions/:id — Delete version
|
|
||||||
* - GET /projects/:projectId/versions/compare — Compare two versions
|
|
||||||
*/
|
|
||||||
@ApiTags('SkriptAI - Versions')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Controller('skriptai/projects/:projectId/versions')
|
|
||||||
export class VersionsController {
|
|
||||||
private readonly logger = new Logger(VersionsController.name);
|
|
||||||
|
|
||||||
constructor(private readonly versionsService: VersionsService) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@ApiOperation({ summary: 'List all versions for a project' })
|
|
||||||
@ApiParam({ name: 'projectId', description: 'Project ID' })
|
|
||||||
async listVersions(@Param('projectId') projectId: string) {
|
|
||||||
return this.versionsService.listVersions(projectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('compare')
|
|
||||||
@ApiOperation({ summary: 'Compare two versions' })
|
|
||||||
async compareVersions(
|
|
||||||
@Param('projectId') projectId: string,
|
|
||||||
@Query('versionA') versionAId: string,
|
|
||||||
@Query('versionB') versionBId: string,
|
|
||||||
) {
|
|
||||||
return this.versionsService.compareVersions(
|
|
||||||
projectId,
|
|
||||||
versionAId,
|
|
||||||
versionBId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':id')
|
|
||||||
@ApiOperation({ summary: 'Get a specific version with full snapshot data' })
|
|
||||||
async getVersion(
|
|
||||||
@Param('projectId') projectId: string,
|
|
||||||
@Param('id') versionId: string,
|
|
||||||
) {
|
|
||||||
return this.versionsService.getVersion(projectId, versionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
@ApiOperation({ summary: 'Manually save current state as a new version' })
|
|
||||||
async createSnapshot(
|
|
||||||
@Param('projectId') projectId: string,
|
|
||||||
@Body() body: { label?: string; changeNote?: string },
|
|
||||||
) {
|
|
||||||
return this.versionsService.createSnapshot(
|
|
||||||
projectId,
|
|
||||||
'USER',
|
|
||||||
body.label,
|
|
||||||
body.changeNote,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post(':id/restore')
|
|
||||||
@ApiOperation({ summary: 'Restore project to a specific version' })
|
|
||||||
async restoreVersion(
|
|
||||||
@Param('projectId') projectId: string,
|
|
||||||
@Param('id') versionId: string,
|
|
||||||
) {
|
|
||||||
return this.versionsService.restoreVersion(projectId, versionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(':id')
|
|
||||||
@ApiOperation({ summary: 'Delete a specific version' })
|
|
||||||
async deleteVersion(
|
|
||||||
@Param('projectId') projectId: string,
|
|
||||||
@Param('id') versionId: string,
|
|
||||||
) {
|
|
||||||
return this.versionsService.deleteVersion(projectId, versionId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from './project.dto';
|
|
||||||
export * from './segment.dto';
|
|
||||||
export * from './research.dto';
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
||||||
import {
|
|
||||||
IsString,
|
|
||||||
IsOptional,
|
|
||||||
IsArray,
|
|
||||||
IsBoolean,
|
|
||||||
ArrayMinSize,
|
|
||||||
} from 'class-validator';
|
|
||||||
|
|
||||||
// Types are defined as string unions in skriptai.types.ts
|
|
||||||
// Using string here to avoid emitDecoratorMetadata issues
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CreateProjectDto
|
|
||||||
*
|
|
||||||
* DTO for creating a new script project.
|
|
||||||
*
|
|
||||||
* TR: Yeni bir script projesi oluşturmak için DTO.
|
|
||||||
* EN: DTO for creating a new script project.
|
|
||||||
*/
|
|
||||||
export class CreateProjectDto {
|
|
||||||
@ApiProperty({ description: 'Main topic of the project' })
|
|
||||||
@IsString()
|
|
||||||
topic: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Content format type' })
|
|
||||||
@IsString()
|
|
||||||
contentType: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Target audience list', isArray: true })
|
|
||||||
@IsArray()
|
|
||||||
@ArrayMinSize(1)
|
|
||||||
targetAudience: string[];
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Speech style list', isArray: true })
|
|
||||||
@IsArray()
|
|
||||||
@ArrayMinSize(1)
|
|
||||||
speechStyle: string[];
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Target video duration' })
|
|
||||||
@IsString()
|
|
||||||
targetDuration: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Additional user notes' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
userNotes?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Content tone' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
tone?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Content language', default: 'tr' })
|
|
||||||
@IsString()
|
|
||||||
language: string = 'tr';
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Include interview segments' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
includeInterviews?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UpdateProjectDto
|
|
||||||
*
|
|
||||||
* DTO for updating an existing project.
|
|
||||||
* All fields are optional.
|
|
||||||
*
|
|
||||||
* TR: Mevcut bir projeyi güncellemek için DTO.
|
|
||||||
* EN: DTO for updating an existing project.
|
|
||||||
*/
|
|
||||||
export class UpdateProjectDto {
|
|
||||||
@ApiPropertyOptional({ description: 'Main topic of the project' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
topic?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Content format type' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
contentType?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Target audience list', isArray: true })
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
targetAudience?: string[];
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Speech style list', isArray: true })
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
speechStyle?: string[];
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Target video duration' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
targetDuration?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Additional user notes' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
userNotes?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Content tone' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
tone?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Content language' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
language?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Project logline' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
logline?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'High concept description' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
highConcept?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Include interview segments' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
includeInterviews?: boolean;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'SEO title' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
seoTitle?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'SEO description' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
seoDescription?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'SEO tags', isArray: true })
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
seoTags?: string[];
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
||||||
import { IsString, IsOptional, IsBoolean, IsArray } from 'class-validator';
|
|
||||||
|
|
||||||
// SourceType and CharacterRole are string unions - using string here for decorator compatibility
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CreateSourceDto
|
|
||||||
*
|
|
||||||
* DTO for adding a research source.
|
|
||||||
*
|
|
||||||
* TR: Araştırma kaynağı eklemek için DTO.
|
|
||||||
* EN: DTO for adding a research source.
|
|
||||||
*/
|
|
||||||
export class CreateSourceDto {
|
|
||||||
@ApiProperty({ description: 'Project ID' })
|
|
||||||
@IsString()
|
|
||||||
projectId: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Source title' })
|
|
||||||
@IsString()
|
|
||||||
title: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Source URL' })
|
|
||||||
@IsString()
|
|
||||||
url: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Source snippet/summary' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
snippet?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Source type' })
|
|
||||||
@IsString()
|
|
||||||
type: string; // article, video, interview, etc.
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Whether source is selected' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
selected?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CreateBriefItemDto
|
|
||||||
*
|
|
||||||
* DTO for adding a brief question/answer.
|
|
||||||
*
|
|
||||||
* TR: Brief sorusu/cevabı eklemek için DTO.
|
|
||||||
* EN: DTO for adding a brief question/answer.
|
|
||||||
*/
|
|
||||||
export class CreateBriefItemDto {
|
|
||||||
@ApiProperty({ description: 'Project ID' })
|
|
||||||
@IsString()
|
|
||||||
projectId: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Question text' })
|
|
||||||
@IsString()
|
|
||||||
question: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Answer text' })
|
|
||||||
@IsString()
|
|
||||||
answer: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Sort order' })
|
|
||||||
@IsOptional()
|
|
||||||
sortOrder?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CreateCharacterDto
|
|
||||||
*
|
|
||||||
* DTO for creating a character profile.
|
|
||||||
*
|
|
||||||
* TR: Karakter profili oluşturmak için DTO.
|
|
||||||
* EN: DTO for creating a character profile.
|
|
||||||
*/
|
|
||||||
export class CreateCharacterDto {
|
|
||||||
@ApiProperty({ description: 'Project ID' })
|
|
||||||
@IsString()
|
|
||||||
projectId: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Character name' })
|
|
||||||
@IsString()
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Character role' })
|
|
||||||
@IsString()
|
|
||||||
role: string; // Protagonist, Antagonist, etc.
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Character values (inner beliefs)' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
values?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Character traits (personality)' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
traits?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
description: 'Character mannerisms (external behavior)',
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
mannerisms?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PerformResearchDto
|
|
||||||
*
|
|
||||||
* DTO for performing deep research.
|
|
||||||
*
|
|
||||||
* TR: Derin araştırma yapmak için DTO.
|
|
||||||
* EN: DTO for performing deep research.
|
|
||||||
*/
|
|
||||||
export class PerformResearchDto {
|
|
||||||
@ApiProperty({ description: 'Project ID' })
|
|
||||||
@IsString()
|
|
||||||
projectId: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Additional research query' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
additionalQuery?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GenerateDiscoveryQuestionsDto
|
|
||||||
*
|
|
||||||
* DTO for generating creative brief discovery questions.
|
|
||||||
*
|
|
||||||
* TR: Yaratıcı brief keşif soruları oluşturmak için DTO.
|
|
||||||
* EN: DTO for generating creative brief discovery questions.
|
|
||||||
*/
|
|
||||||
export class GenerateDiscoveryQuestionsDto {
|
|
||||||
@ApiProperty({ description: 'Topic to generate questions for' })
|
|
||||||
@IsString()
|
|
||||||
topic: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Language for questions' })
|
|
||||||
@IsString()
|
|
||||||
language: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Existing questions to avoid' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
existingQuestions?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GenerateLoglineDto
|
|
||||||
*
|
|
||||||
* DTO for generating logline and high concept.
|
|
||||||
*
|
|
||||||
* TR: Logline ve high concept oluşturmak için DTO.
|
|
||||||
* EN: DTO for generating logline and high concept.
|
|
||||||
*/
|
|
||||||
export class GenerateLoglineDto {
|
|
||||||
@ApiProperty({ description: 'Project ID' })
|
|
||||||
@IsString()
|
|
||||||
projectId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GenerateCharactersDto
|
|
||||||
*
|
|
||||||
* DTO for auto-generating character profiles.
|
|
||||||
*
|
|
||||||
* TR: Otomatik karakter profilleri oluşturmak için DTO.
|
|
||||||
* EN: DTO for auto-generating character profiles.
|
|
||||||
*/
|
|
||||||
export class GenerateCharactersDto {
|
|
||||||
@ApiProperty({ description: 'Project ID' })
|
|
||||||
@IsString()
|
|
||||||
projectId: string;
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
||||||
import { IsString, IsOptional, IsInt, Min } from 'class-validator';
|
|
||||||
|
|
||||||
// SegmentType and SpeechStyle are string unions - using string here for decorator compatibility
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CreateSegmentDto
|
|
||||||
*
|
|
||||||
* DTO for creating a new script segment.
|
|
||||||
*
|
|
||||||
* TR: Yeni bir script segmenti oluşturmak için DTO.
|
|
||||||
* EN: DTO for creating a new script segment.
|
|
||||||
*/
|
|
||||||
export class CreateSegmentDto {
|
|
||||||
@ApiProperty({ description: 'Project ID this segment belongs to' })
|
|
||||||
@IsString()
|
|
||||||
projectId: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Segment type (Hook, Intro, Body, etc.)' })
|
|
||||||
@IsString()
|
|
||||||
segmentType: string; // Hook, Intro, Body, etc.
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Start time in format MM:SS' })
|
|
||||||
@IsString()
|
|
||||||
timeStart: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Duration in seconds (e.g., "30s")' })
|
|
||||||
@IsString()
|
|
||||||
duration: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Visual description for the segment' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
visualDescription?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Narrator script text' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
narratorScript?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Editor notes' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
editorNotes?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Audio cues' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
audioCues?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'On-screen text overlay' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
onScreenText?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Stock footage search query' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
stockQuery?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Video generation prompt (VEO/Runway)' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
videoPrompt?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
description: 'Image generation prompt (Midjourney/Flux)',
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
imagePrompt?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Sort order in the script' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsInt()
|
|
||||||
@Min(0)
|
|
||||||
sortOrder?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UpdateSegmentDto
|
|
||||||
*
|
|
||||||
* DTO for updating an existing segment.
|
|
||||||
*
|
|
||||||
* TR: Mevcut bir segmenti güncellemek için DTO.
|
|
||||||
* EN: DTO for updating an existing segment.
|
|
||||||
*/
|
|
||||||
export class UpdateSegmentDto {
|
|
||||||
@ApiPropertyOptional({ description: 'Segment type' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
segmentType?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Start time' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
timeStart?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Duration' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
duration?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Visual description' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
visualDescription?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Narrator script' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
narratorScript?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Editor notes' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
editorNotes?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'General notes' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
generalNotes?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Audio cues' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
audioCues?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'On-screen text' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
onScreenText?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Stock query' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
stockQuery?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Video prompt' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
videoPrompt?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Image prompt' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
imagePrompt?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Generated image URL' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
generatedImageUrl?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Sort order' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsInt()
|
|
||||||
@Min(0)
|
|
||||||
sortOrder?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RewriteSegmentDto
|
|
||||||
*
|
|
||||||
* DTO for rewriting a segment with a new style.
|
|
||||||
*
|
|
||||||
* TR: Bir segmenti yeni bir stille yeniden yazmak için DTO.
|
|
||||||
* EN: DTO for rewriting a segment with a new style.
|
|
||||||
*/
|
|
||||||
export class RewriteSegmentDto {
|
|
||||||
@ApiProperty({ description: 'Segment ID to rewrite' })
|
|
||||||
@IsString()
|
|
||||||
segmentId: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'New style to apply' })
|
|
||||||
@IsString()
|
|
||||||
newStyle: string; // SpeechStyle or 'Make it Longer' | 'Make it Shorter'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GenerateScriptDto
|
|
||||||
*
|
|
||||||
* DTO for generating a full script from project data.
|
|
||||||
*
|
|
||||||
* TR: Proje verisinden tam bir script oluşturmak için DTO.
|
|
||||||
* EN: DTO for generating a full script from project data.
|
|
||||||
*/
|
|
||||||
export class GenerateScriptDto {
|
|
||||||
@ApiProperty({ description: 'Project ID to generate script for' })
|
|
||||||
@IsString()
|
|
||||||
projectId: string;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export * from './skriptai.module';
|
|
||||||
export * from './services';
|
|
||||||
export * from './controllers';
|
|
||||||
export * from './dto';
|
|
||||||
export * from './types/skriptai.types';
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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", ...]',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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": "..." }]',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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" }]',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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"] }',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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';
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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": "..." }',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
||||||
import { PrismaService } from '../../../database/prisma.service';
|
|
||||||
import { GeminiService } from '../../gemini/gemini.service';
|
|
||||||
import { NeuroAnalysisResult, YoutubeAudit } from '../types/skriptai.types';
|
|
||||||
import {
|
|
||||||
buildNeuroAnalysisPrompt,
|
|
||||||
buildYoutubeAuditPrompt,
|
|
||||||
buildCommercialBriefPrompt,
|
|
||||||
buildVisualAssetKeywordsPrompt,
|
|
||||||
} from '../prompts';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AnalysisService
|
|
||||||
*
|
|
||||||
* Service for AI-powered content analysis including:
|
|
||||||
* - Neuro Marketing Analysis
|
|
||||||
* - YouTube Audit
|
|
||||||
* - Commercial Brief Generation
|
|
||||||
*
|
|
||||||
* TR: AI destekli içerik analizi servisi (Nöro Pazarlama, YouTube Denetimi, Ticari Brief).
|
|
||||||
* EN: Service for AI-powered content analysis.
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class AnalysisService {
|
|
||||||
private readonly logger = new Logger(AnalysisService.name);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly gemini: GeminiService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform Neuro Marketing Analysis on a script
|
|
||||||
*
|
|
||||||
* @param projectId - Project ID
|
|
||||||
* @returns Neuro analysis result
|
|
||||||
*/
|
|
||||||
async analyzeNeuroMarketing(projectId: string): Promise<NeuroAnalysisResult> {
|
|
||||||
const project = await this.prisma.scriptProject.findUnique({
|
|
||||||
where: { id: projectId },
|
|
||||||
include: { segments: { orderBy: { sortOrder: 'asc' } } },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullScript = project.segments
|
|
||||||
.map((s) => s.narratorScript)
|
|
||||||
.join('\n\n');
|
|
||||||
|
|
||||||
const promptData = buildNeuroAnalysisPrompt({ fullScript });
|
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<NeuroAnalysisResult>(
|
|
||||||
promptData.prompt,
|
|
||||||
promptData.schema,
|
|
||||||
{ temperature: promptData.temperature },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save to project
|
|
||||||
await this.prisma.scriptProject.update({
|
|
||||||
where: { id: projectId },
|
|
||||||
data: { neuroAnalysis: resp.data as any },
|
|
||||||
});
|
|
||||||
|
|
||||||
return resp.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform YouTube Audit
|
|
||||||
*
|
|
||||||
* @param projectId - Project ID
|
|
||||||
* @returns YouTube audit result
|
|
||||||
*/
|
|
||||||
async performYoutubeAudit(projectId: string): Promise<YoutubeAudit> {
|
|
||||||
const project = await this.prisma.scriptProject.findUnique({
|
|
||||||
where: { id: projectId },
|
|
||||||
include: { segments: { orderBy: { sortOrder: 'asc' } } },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullScript = project.segments
|
|
||||||
.map((s) => s.narratorScript)
|
|
||||||
.join('\n\n');
|
|
||||||
|
|
||||||
const promptData = buildYoutubeAuditPrompt({
|
|
||||||
topic: project.topic,
|
|
||||||
fullScript,
|
|
||||||
});
|
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<YoutubeAudit>(
|
|
||||||
promptData.prompt,
|
|
||||||
promptData.schema,
|
|
||||||
{ temperature: promptData.temperature },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save to project
|
|
||||||
await this.prisma.scriptProject.update({
|
|
||||||
where: { id: projectId },
|
|
||||||
data: { youtubeAudit: resp.data as any },
|
|
||||||
});
|
|
||||||
|
|
||||||
return resp.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate Commercial Brief (Sponsorship Analysis)
|
|
||||||
*
|
|
||||||
* @param projectId - Project ID
|
|
||||||
* @returns Commercial brief with sponsor suggestions
|
|
||||||
*/
|
|
||||||
async generateCommercialBrief(projectId: string) {
|
|
||||||
const project = await this.prisma.scriptProject.findUnique({
|
|
||||||
where: { id: projectId },
|
|
||||||
include: { segments: { orderBy: { sortOrder: 'asc' } } },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullScript = project.segments
|
|
||||||
.map((s) => s.narratorScript)
|
|
||||||
.join('\n\n');
|
|
||||||
|
|
||||||
const promptData = buildCommercialBriefPrompt({
|
|
||||||
topic: project.topic,
|
|
||||||
targetAudience: project.targetAudience,
|
|
||||||
contentType: project.contentType,
|
|
||||||
fullScript,
|
|
||||||
});
|
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<{
|
|
||||||
viabilityScore: string;
|
|
||||||
viabilityReason: string;
|
|
||||||
sponsors: {
|
|
||||||
name: string;
|
|
||||||
industry: string;
|
|
||||||
matchReason: string;
|
|
||||||
emailDraft: string;
|
|
||||||
}[];
|
|
||||||
}>(promptData.prompt, promptData.schema, {
|
|
||||||
temperature: promptData.temperature,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save to project
|
|
||||||
await this.prisma.scriptProject.update({
|
|
||||||
where: { id: projectId },
|
|
||||||
data: { commercialBrief: resp.data as any },
|
|
||||||
});
|
|
||||||
|
|
||||||
return resp.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate thumbnails using external image service
|
|
||||||
* Applies "Nano Banana" prompt enrichment for high-quality results.
|
|
||||||
*
|
|
||||||
* @param prompt - Image generation prompt
|
|
||||||
* @returns Generated image URL
|
|
||||||
*/
|
|
||||||
async generateThumbnailImage(prompt: string): Promise<string> {
|
|
||||||
// Quality boosters (Nano Banana style)
|
|
||||||
const QUALITY_BOOSTERS = [
|
|
||||||
'highly detailed',
|
|
||||||
'8k resolution',
|
|
||||||
'professional photography',
|
|
||||||
'studio lighting',
|
|
||||||
'sharp focus',
|
|
||||||
'cinematic composition',
|
|
||||||
'vibrant colors',
|
|
||||||
'masterpiece',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Enrich prompt with Nano Banana logic
|
|
||||||
const enrichedPrompt = `${prompt}, ${QUALITY_BOOSTERS.join(', ')}. CRITICAL OBJECTIVE: The result MUST achieve a perfect 10/10 score. Clarity: 10/10. Professionalism: 10/10.`;
|
|
||||||
|
|
||||||
// Use Real Nano Banana (Gemini Imagen)
|
|
||||||
return await this.gemini.generateImage(enrichedPrompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate visual assets for a project
|
|
||||||
*
|
|
||||||
* @param projectId - Project ID
|
|
||||||
* @param count - Number of assets to generate
|
|
||||||
* @returns Generated visual assets
|
|
||||||
*/
|
|
||||||
async generateVisualAssets(projectId: string, count: number = 5) {
|
|
||||||
const project = await this.prisma.scriptProject.findUnique({
|
|
||||||
where: { id: projectId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const promptData = buildVisualAssetKeywordsPrompt({
|
|
||||||
topic: project.topic,
|
|
||||||
count,
|
|
||||||
});
|
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<string[]>(
|
|
||||||
promptData.prompt,
|
|
||||||
promptData.schema,
|
|
||||||
{ temperature: promptData.temperature },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate image URLs and save to database
|
|
||||||
const assets = await Promise.all(
|
|
||||||
resp.data.map(async (keyword) => {
|
|
||||||
const url = await this.generateThumbnailImage(keyword);
|
|
||||||
return this.prisma.visualAsset.create({
|
|
||||||
data: {
|
|
||||||
projectId,
|
|
||||||
url,
|
|
||||||
desc: keyword,
|
|
||||||
selected: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return assets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export * from './projects.service';
|
|
||||||
export * from './scripts.service';
|
|
||||||
export * from './research.service';
|
|
||||||
export * from './analysis.service';
|
|
||||||
export * from './versions.service';
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
||||||
import { PrismaService } from '../../../database/prisma.service';
|
|
||||||
import { CreateProjectDto, UpdateProjectDto } from '../dto';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ProjectsService
|
|
||||||
*
|
|
||||||
* Service for managing script projects (CRUD operations).
|
|
||||||
*
|
|
||||||
* TR: Script projelerini yönetmek için servis (CRUD operasyonları).
|
|
||||||
* EN: Service for managing script projects (CRUD operations).
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class ProjectsService {
|
|
||||||
private readonly logger = new Logger(ProjectsService.name);
|
|
||||||
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new script project
|
|
||||||
*
|
|
||||||
* @param data - Project creation data
|
|
||||||
* @param userId - Optional user ID for ownership
|
|
||||||
* @returns Created project
|
|
||||||
*/
|
|
||||||
async create(data: CreateProjectDto, userId?: string) {
|
|
||||||
this.logger.log(`Creating project: ${data.topic}`);
|
|
||||||
|
|
||||||
return this.prisma.scriptProject.create({
|
|
||||||
data: {
|
|
||||||
...data,
|
|
||||||
userId,
|
|
||||||
seoTags: [],
|
|
||||||
thumbnailIdeas: [],
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
segments: true,
|
|
||||||
sources: true,
|
|
||||||
characters: true,
|
|
||||||
briefItems: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all projects for a user
|
|
||||||
*
|
|
||||||
* @param userId - User ID (optional, returns all if not provided)
|
|
||||||
* @returns List of projects
|
|
||||||
*/
|
|
||||||
async findAll(userId?: string) {
|
|
||||||
const where = userId ? { userId, deletedAt: null } : { deletedAt: null };
|
|
||||||
|
|
||||||
return this.prisma.scriptProject.findMany({
|
|
||||||
where,
|
|
||||||
orderBy: { updatedAt: 'desc' },
|
|
||||||
include: {
|
|
||||||
_count: {
|
|
||||||
select: {
|
|
||||||
segments: true,
|
|
||||||
sources: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a project by ID
|
|
||||||
*
|
|
||||||
* @param id - Project ID
|
|
||||||
* @returns Project with all relations
|
|
||||||
*/
|
|
||||||
async findOne(id: string) {
|
|
||||||
const project = await this.prisma.scriptProject.findUnique({
|
|
||||||
where: { id },
|
|
||||||
include: {
|
|
||||||
segments: { orderBy: { sortOrder: 'asc' } },
|
|
||||||
sources: true,
|
|
||||||
characters: true,
|
|
||||||
briefItems: { orderBy: { sortOrder: 'asc' } },
|
|
||||||
visualAssets: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
throw new NotFoundException(`Project with ID ${id} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return project;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a project
|
|
||||||
*
|
|
||||||
* @param id - Project ID
|
|
||||||
* @param data - Update data
|
|
||||||
* @returns Updated project
|
|
||||||
*/
|
|
||||||
async update(id: string, data: UpdateProjectDto) {
|
|
||||||
// Verify project exists
|
|
||||||
await this.findOne(id);
|
|
||||||
|
|
||||||
return this.prisma.scriptProject.update({
|
|
||||||
where: { id },
|
|
||||||
data,
|
|
||||||
include: {
|
|
||||||
segments: { orderBy: { sortOrder: 'asc' } },
|
|
||||||
sources: true,
|
|
||||||
characters: true,
|
|
||||||
briefItems: { orderBy: { sortOrder: 'asc' } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Soft delete a project
|
|
||||||
*
|
|
||||||
* @param id - Project ID
|
|
||||||
*/
|
|
||||||
async remove(id: string) {
|
|
||||||
await this.findOne(id);
|
|
||||||
|
|
||||||
return this.prisma.scriptProject.update({
|
|
||||||
where: { id },
|
|
||||||
data: { deletedAt: new Date() },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hard delete a project and all related data
|
|
||||||
*
|
|
||||||
* @param id - Project ID
|
|
||||||
*/
|
|
||||||
async hardDelete(id: string) {
|
|
||||||
await this.findOne(id);
|
|
||||||
|
|
||||||
return this.prisma.scriptProject.delete({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Duplicate a project with all its content
|
|
||||||
*
|
|
||||||
* @param id - Project ID to duplicate
|
|
||||||
* @param userId - New owner user ID
|
|
||||||
* @returns New duplicated project
|
|
||||||
*/
|
|
||||||
async duplicate(id: string, userId?: string) {
|
|
||||||
const original = await this.findOne(id);
|
|
||||||
|
|
||||||
// Create new project with copied data
|
|
||||||
const newProject = await this.prisma.scriptProject.create({
|
|
||||||
data: {
|
|
||||||
userId,
|
|
||||||
topic: `${original.topic} (Copy)`,
|
|
||||||
contentType: original.contentType,
|
|
||||||
targetAudience: original.targetAudience,
|
|
||||||
speechStyle: original.speechStyle,
|
|
||||||
targetDuration: original.targetDuration,
|
|
||||||
userNotes: original.userNotes,
|
|
||||||
tone: original.tone,
|
|
||||||
language: original.language,
|
|
||||||
logline: original.logline,
|
|
||||||
highConcept: original.highConcept,
|
|
||||||
includeInterviews: original.includeInterviews,
|
|
||||||
seoTitle: original.seoTitle,
|
|
||||||
seoDescription: original.seoDescription,
|
|
||||||
seoTags: original.seoTags,
|
|
||||||
thumbnailIdeas: original.thumbnailIdeas,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Copy segments
|
|
||||||
if (original.segments.length > 0) {
|
|
||||||
await this.prisma.scriptSegment.createMany({
|
|
||||||
data: original.segments.map((seg) => ({
|
|
||||||
projectId: newProject.id,
|
|
||||||
segmentType: seg.segmentType,
|
|
||||||
timeStart: seg.timeStart,
|
|
||||||
duration: seg.duration,
|
|
||||||
visualDescription: seg.visualDescription,
|
|
||||||
narratorScript: seg.narratorScript,
|
|
||||||
editorNotes: seg.editorNotes,
|
|
||||||
generalNotes: seg.generalNotes,
|
|
||||||
audioCues: seg.audioCues,
|
|
||||||
onScreenText: seg.onScreenText,
|
|
||||||
stockQuery: seg.stockQuery,
|
|
||||||
videoPrompt: seg.videoPrompt,
|
|
||||||
imagePrompt: seg.imagePrompt,
|
|
||||||
sortOrder: seg.sortOrder,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy sources
|
|
||||||
if (original.sources.length > 0) {
|
|
||||||
await this.prisma.researchSource.createMany({
|
|
||||||
data: original.sources.map((src) => ({
|
|
||||||
projectId: newProject.id,
|
|
||||||
title: src.title,
|
|
||||||
url: src.url,
|
|
||||||
snippet: src.snippet,
|
|
||||||
type: src.type,
|
|
||||||
selected: src.selected,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy characters
|
|
||||||
if (original.characters.length > 0) {
|
|
||||||
await this.prisma.characterProfile.createMany({
|
|
||||||
data: original.characters.map((char) => ({
|
|
||||||
projectId: newProject.id,
|
|
||||||
name: char.name,
|
|
||||||
role: char.role,
|
|
||||||
values: char.values,
|
|
||||||
traits: char.traits,
|
|
||||||
mannerisms: char.mannerisms,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy brief items
|
|
||||||
if (original.briefItems.length > 0) {
|
|
||||||
await this.prisma.briefItem.createMany({
|
|
||||||
data: original.briefItems.map((item) => ({
|
|
||||||
projectId: newProject.id,
|
|
||||||
question: item.question,
|
|
||||||
answer: item.answer,
|
|
||||||
sortOrder: item.sortOrder,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.findOne(newProject.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export project to JSON format (for download)
|
|
||||||
*
|
|
||||||
* @param id - Project ID
|
|
||||||
* @returns Project data as JSON-serializable object
|
|
||||||
*/
|
|
||||||
async exportToJson(id: string) {
|
|
||||||
const project = await this.findOne(id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
version: '1.0',
|
|
||||||
project: {
|
|
||||||
topic: project.topic,
|
|
||||||
contentType: project.contentType,
|
|
||||||
targetAudience: project.targetAudience,
|
|
||||||
speechStyle: project.speechStyle,
|
|
||||||
targetDuration: project.targetDuration,
|
|
||||||
userNotes: project.userNotes,
|
|
||||||
tone: project.tone,
|
|
||||||
language: project.language,
|
|
||||||
logline: project.logline,
|
|
||||||
highConcept: project.highConcept,
|
|
||||||
includeInterviews: project.includeInterviews,
|
|
||||||
seo: {
|
|
||||||
title: project.seoTitle,
|
|
||||||
description: project.seoDescription,
|
|
||||||
tags: project.seoTags,
|
|
||||||
thumbnailIdeas: project.thumbnailIdeas,
|
|
||||||
},
|
|
||||||
neuroAnalysis: project.neuroAnalysis,
|
|
||||||
youtubeAudit: project.youtubeAudit,
|
|
||||||
postProduction: project.postProduction,
|
|
||||||
commercialBrief: project.commercialBrief,
|
|
||||||
creativeBrief: project.briefItems.map((item) => ({
|
|
||||||
question: item.question,
|
|
||||||
answer: item.answer,
|
|
||||||
})),
|
|
||||||
sources: project.sources.map((src) => ({
|
|
||||||
title: src.title,
|
|
||||||
url: src.url,
|
|
||||||
snippet: src.snippet,
|
|
||||||
type: src.type,
|
|
||||||
selected: src.selected,
|
|
||||||
})),
|
|
||||||
characters: project.characters.map((char) => ({
|
|
||||||
name: char.name,
|
|
||||||
role: char.role,
|
|
||||||
values: char.values,
|
|
||||||
traits: char.traits,
|
|
||||||
mannerisms: char.mannerisms,
|
|
||||||
})),
|
|
||||||
script: project.segments.map((seg) => ({
|
|
||||||
segmentType: seg.segmentType,
|
|
||||||
timeStart: seg.timeStart,
|
|
||||||
duration: seg.duration,
|
|
||||||
visualDescription: seg.visualDescription,
|
|
||||||
narratorScript: seg.narratorScript,
|
|
||||||
editorNotes: seg.editorNotes,
|
|
||||||
generalNotes: seg.generalNotes,
|
|
||||||
audioCues: seg.audioCues,
|
|
||||||
onScreenText: seg.onScreenText,
|
|
||||||
stockQuery: seg.stockQuery,
|
|
||||||
videoPrompt: seg.videoPrompt,
|
|
||||||
imagePrompt: seg.imagePrompt,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,392 +0,0 @@
|
|||||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
||||||
import { PrismaService } from '../../../database/prisma.service';
|
|
||||||
import { GeminiService } from '../../gemini/gemini.service';
|
|
||||||
import {
|
|
||||||
CreateSourceDto,
|
|
||||||
CreateBriefItemDto,
|
|
||||||
CreateCharacterDto,
|
|
||||||
} from '../dto';
|
|
||||||
import { CharacterRole } from '../types/skriptai.types';
|
|
||||||
import {
|
|
||||||
buildDiscoveryQuestionsPrompt,
|
|
||||||
buildSearchQueryPrompt,
|
|
||||||
buildSourceSearchPrompt,
|
|
||||||
buildCharacterGenerationPrompt,
|
|
||||||
buildLoglinePrompt,
|
|
||||||
} from '../prompts';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ResearchService
|
|
||||||
*
|
|
||||||
* Service for managing research sources, creative brief, and character profiles.
|
|
||||||
* Also provides AI-powered research and discovery question generation.
|
|
||||||
*
|
|
||||||
* TR: Araştırma kaynakları, yaratıcı brief ve karakter profilleri yönetimi.
|
|
||||||
* EN: Service for managing research sources, creative brief, and character profiles.
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class ResearchService {
|
|
||||||
private readonly logger = new Logger(ResearchService.name);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly gemini: GeminiService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
// ========== SOURCES ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a research source
|
|
||||||
*/
|
|
||||||
async addSource(data: CreateSourceDto) {
|
|
||||||
return this.prisma.researchSource.create({ data });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle source selection
|
|
||||||
*/
|
|
||||||
async toggleSourceSelection(id: string) {
|
|
||||||
const source = await this.prisma.researchSource.findUnique({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
if (!source) {
|
|
||||||
throw new NotFoundException(`Source with ID ${id} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.prisma.researchSource.update({
|
|
||||||
where: { id },
|
|
||||||
data: { selected: !source.selected },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a source
|
|
||||||
*/
|
|
||||||
async deleteSource(id: string) {
|
|
||||||
return this.prisma.researchSource.delete({ where: { id } });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform deep research using AI
|
|
||||||
*
|
|
||||||
* @param projectId - Project ID
|
|
||||||
* @param additionalQuery - Optional additional query to append
|
|
||||||
* @returns Generated research sources
|
|
||||||
*/
|
|
||||||
async performDeepResearch(projectId: string, additionalQuery?: string) {
|
|
||||||
const project = await this.prisma.scriptProject.findUnique({
|
|
||||||
where: { id: projectId },
|
|
||||||
include: { briefItems: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const briefContext = project.briefItems
|
|
||||||
.map((b) => `Q: ${b.question} A: ${b.answer}`)
|
|
||||||
.join('; ');
|
|
||||||
|
|
||||||
const topic = additionalQuery
|
|
||||||
? `${project.topic} ${additionalQuery}`
|
|
||||||
: project.topic;
|
|
||||||
|
|
||||||
// Generate search queries
|
|
||||||
let searchQueries: string[] = [];
|
|
||||||
|
|
||||||
if (!this.gemini.isAvailable()) {
|
|
||||||
this.logger.warn('Gemini is disabled. Using mock search queries.');
|
|
||||||
searchQueries = [
|
|
||||||
`${topic} foundation`,
|
|
||||||
`${topic} controversy`,
|
|
||||||
`${topic} future`,
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const queryPromptData = buildSearchQueryPrompt({
|
|
||||||
topic,
|
|
||||||
briefContext,
|
|
||||||
language: project.language,
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryResp = await this.gemini.generateJSON<string[]>(
|
|
||||||
queryPromptData.prompt,
|
|
||||||
queryPromptData.schema,
|
|
||||||
{
|
|
||||||
temperature: queryPromptData.temperature,
|
|
||||||
tools: [{ googleSearch: {} }],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
searchQueries = queryResp.data;
|
|
||||||
} catch {
|
|
||||||
searchQueries = [`${topic} details`, `${topic} news`];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate sources for each query
|
|
||||||
const allSources: any[] = [];
|
|
||||||
const processedUrls = new Set<string>();
|
|
||||||
|
|
||||||
for (const query of searchQueries.slice(0, 5)) {
|
|
||||||
try {
|
|
||||||
if (!this.gemini.isAvailable()) {
|
|
||||||
allSources.push({
|
|
||||||
projectId,
|
|
||||||
title: `Mock Source: ${query}`,
|
|
||||||
url: `https://example.com/mock/${query.replace(/\s+/g, '-')}`,
|
|
||||||
snippet: `This is a simulated research result for "${query}" because AI is disabled. Enable Gemini in .env for real results.`,
|
|
||||||
type: 'article',
|
|
||||||
selected: true,
|
|
||||||
isNew: true,
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourcePromptData = buildSourceSearchPrompt({
|
|
||||||
query,
|
|
||||||
language: project.language,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sourceResp = await this.gemini.generateJSON<
|
|
||||||
{ title: string; url: string; snippet: string; type: string }[]
|
|
||||||
>(sourcePromptData.prompt, sourcePromptData.schema, {
|
|
||||||
temperature: sourcePromptData.temperature,
|
|
||||||
tools: [{ googleSearch: {} }],
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const item of sourceResp.data) {
|
|
||||||
if (item.url && !processedUrls.has(item.url)) {
|
|
||||||
processedUrls.add(item.url);
|
|
||||||
allSources.push({
|
|
||||||
projectId,
|
|
||||||
title: item.title,
|
|
||||||
url: item.url,
|
|
||||||
snippet: item.snippet,
|
|
||||||
type: item.type || 'article',
|
|
||||||
selected: true,
|
|
||||||
isNew: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
this.logger.warn(`Failed to get sources for query: ${query}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save sources to database
|
|
||||||
if (allSources.length > 0) {
|
|
||||||
await this.prisma.researchSource.createMany({
|
|
||||||
data: allSources,
|
|
||||||
skipDuplicates: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.prisma.researchSource.findMany({
|
|
||||||
where: { projectId },
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== BRIEF ITEMS ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a brief item (Q&A)
|
|
||||||
*/
|
|
||||||
async addBriefItem(data: CreateBriefItemDto) {
|
|
||||||
const lastItem = await this.prisma.briefItem.findFirst({
|
|
||||||
where: { projectId: data.projectId },
|
|
||||||
orderBy: { sortOrder: 'desc' },
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.prisma.briefItem.create({
|
|
||||||
data: {
|
|
||||||
...data,
|
|
||||||
sortOrder: data.sortOrder ?? (lastItem?.sortOrder ?? 0) + 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a brief item
|
|
||||||
*/
|
|
||||||
async updateBriefItem(id: string, answer: string) {
|
|
||||||
return this.prisma.briefItem.update({
|
|
||||||
where: { id },
|
|
||||||
data: { answer },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a brief item
|
|
||||||
*/
|
|
||||||
async deleteBriefItem(id: string) {
|
|
||||||
return this.prisma.briefItem.delete({ where: { id } });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate discovery questions using AI
|
|
||||||
*
|
|
||||||
* @param topic - Topic to generate questions for
|
|
||||||
* @param language - Language for questions
|
|
||||||
* @param existingQuestions - Questions to avoid
|
|
||||||
* @returns Generated questions
|
|
||||||
*/
|
|
||||||
async generateDiscoveryQuestions(
|
|
||||||
topic: string,
|
|
||||||
language: string,
|
|
||||||
existingQuestions: string[] = [],
|
|
||||||
) {
|
|
||||||
// Check if Gemini is available
|
|
||||||
if (!this.gemini.isAvailable()) {
|
|
||||||
this.logger.warn(
|
|
||||||
'Gemini is disabled. Returning mock discovery questions.',
|
|
||||||
);
|
|
||||||
return [
|
|
||||||
`What is the unique angle or "Simpsons Moment" that makes ${topic} surprising?`,
|
|
||||||
`Who is the "Unseen Character" in this story about ${topic}?`,
|
|
||||||
`If ${topic} was a crime scene, what is the smoking gun?`,
|
|
||||||
`What is the one thing everyone gets wrong about ${topic}?`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const promptData = buildDiscoveryQuestionsPrompt({
|
|
||||||
topic,
|
|
||||||
language,
|
|
||||||
existingQuestions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<{ questions: string[] }>(
|
|
||||||
promptData.prompt,
|
|
||||||
promptData.schema,
|
|
||||||
{ temperature: promptData.temperature },
|
|
||||||
);
|
|
||||||
|
|
||||||
return resp.data.questions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== CHARACTERS ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a character profile
|
|
||||||
*/
|
|
||||||
async addCharacter(data: CreateCharacterDto) {
|
|
||||||
return this.prisma.characterProfile.create({ data });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a character
|
|
||||||
*/
|
|
||||||
async updateCharacter(
|
|
||||||
id: string,
|
|
||||||
data: Partial<Omit<CreateCharacterDto, 'projectId'>>,
|
|
||||||
) {
|
|
||||||
return this.prisma.characterProfile.update({
|
|
||||||
where: { id },
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a character
|
|
||||||
*/
|
|
||||||
async deleteCharacter(id: string) {
|
|
||||||
return this.prisma.characterProfile.delete({ where: { id } });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Auto-generate character profiles using AI
|
|
||||||
*
|
|
||||||
* @param projectId - Project ID
|
|
||||||
* @returns Generated characters
|
|
||||||
*/
|
|
||||||
async generateCharacters(projectId: string) {
|
|
||||||
const project = await this.prisma.scriptProject.findUnique({
|
|
||||||
where: { id: projectId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const promptData = buildCharacterGenerationPrompt({
|
|
||||||
contentType: project.contentType,
|
|
||||||
topic: project.topic,
|
|
||||||
language: project.language,
|
|
||||||
});
|
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<
|
|
||||||
{
|
|
||||||
name: string;
|
|
||||||
role: CharacterRole;
|
|
||||||
values: string;
|
|
||||||
traits: string;
|
|
||||||
mannerisms: string;
|
|
||||||
}[]
|
|
||||||
>(promptData.prompt, promptData.schema, {
|
|
||||||
temperature: promptData.temperature,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save characters to database
|
|
||||||
const characters = await Promise.all(
|
|
||||||
resp.data.map((char) =>
|
|
||||||
this.prisma.characterProfile.create({
|
|
||||||
data: {
|
|
||||||
projectId,
|
|
||||||
name: char.name,
|
|
||||||
role: char.role,
|
|
||||||
values: char.values,
|
|
||||||
traits: char.traits,
|
|
||||||
mannerisms: char.mannerisms,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return characters;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== LOGLINE ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate logline and high concept
|
|
||||||
*
|
|
||||||
* @param projectId - Project ID
|
|
||||||
* @returns Logline and high concept
|
|
||||||
*/
|
|
||||||
async generateLogline(projectId: string) {
|
|
||||||
const project = await this.prisma.scriptProject.findUnique({
|
|
||||||
where: { id: projectId },
|
|
||||||
include: { sources: { where: { selected: true }, take: 5 } },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceContext = project.sources.map((s) => s.snippet).join('\n');
|
|
||||||
|
|
||||||
const promptData = buildLoglinePrompt({
|
|
||||||
topic: project.topic,
|
|
||||||
sourceContext,
|
|
||||||
language: project.language,
|
|
||||||
});
|
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<{
|
|
||||||
logline: string;
|
|
||||||
highConcept: string;
|
|
||||||
}>(promptData.prompt, promptData.schema, {
|
|
||||||
temperature: promptData.temperature,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update project
|
|
||||||
await this.prisma.scriptProject.update({
|
|
||||||
where: { id: projectId },
|
|
||||||
data: {
|
|
||||||
logline: resp.data.logline,
|
|
||||||
highConcept: resp.data.highConcept,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return resp.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,482 +0,0 @@
|
|||||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
||||||
import { PrismaService } from '../../../database/prisma.service';
|
|
||||||
import { GeminiService } from '../../gemini/gemini.service';
|
|
||||||
import { CreateSegmentDto, UpdateSegmentDto } from '../dto';
|
|
||||||
import { AnalysisService } from './analysis.service';
|
|
||||||
import { VersionsService } from './versions.service';
|
|
||||||
import {
|
|
||||||
buildScriptOutlinePrompt,
|
|
||||||
buildChapterSegmentPrompt,
|
|
||||||
buildSegmentRewritePrompt,
|
|
||||||
buildSegmentImagePrompt,
|
|
||||||
calculateTargetWordCount,
|
|
||||||
calculateEstimatedChapters,
|
|
||||||
} from '../prompts';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ScriptsService
|
|
||||||
*
|
|
||||||
* Service for managing script segments and AI-powered script generation.
|
|
||||||
*
|
|
||||||
* TR: Script segmentlerini yönetmek ve AI destekli script oluşturmak için servis.
|
|
||||||
* EN: Service for managing script segments and AI-powered script generation.
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class ScriptsService {
|
|
||||||
private readonly logger = new Logger(ScriptsService.name);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly gemini: GeminiService,
|
|
||||||
private readonly analysisService: AnalysisService,
|
|
||||||
private readonly versionsService: VersionsService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new segment
|
|
||||||
*/
|
|
||||||
async createSegment(data: CreateSegmentDto) {
|
|
||||||
// Get highest sortOrder for this project
|
|
||||||
const lastSegment = await this.prisma.scriptSegment.findFirst({
|
|
||||||
where: { projectId: data.projectId },
|
|
||||||
orderBy: { sortOrder: 'desc' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortOrder = data.sortOrder ?? (lastSegment?.sortOrder ?? 0) + 1;
|
|
||||||
|
|
||||||
return this.prisma.scriptSegment.create({
|
|
||||||
data: {
|
|
||||||
...data,
|
|
||||||
sortOrder,
|
|
||||||
citationIndexes: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a segment
|
|
||||||
*/
|
|
||||||
async updateSegment(id: string, data: UpdateSegmentDto) {
|
|
||||||
const segment = await this.prisma.scriptSegment.findUnique({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
if (!segment) {
|
|
||||||
throw new NotFoundException(`Segment with ID ${id} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.prisma.scriptSegment.update({
|
|
||||||
where: { id },
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a segment
|
|
||||||
*/
|
|
||||||
async deleteSegment(id: string) {
|
|
||||||
const segment = await this.prisma.scriptSegment.findUnique({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
if (!segment) {
|
|
||||||
throw new NotFoundException(`Segment with ID ${id} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.prisma.scriptSegment.delete({ where: { id } });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reorder segments
|
|
||||||
*
|
|
||||||
* @param projectId - Project ID
|
|
||||||
* @param segmentIds - Array of segment IDs in new order
|
|
||||||
*/
|
|
||||||
async reorderSegments(projectId: string, segmentIds: string[]) {
|
|
||||||
const updates = segmentIds.map((id, index) =>
|
|
||||||
this.prisma.scriptSegment.update({
|
|
||||||
where: { id },
|
|
||||||
data: { sortOrder: index },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.prisma.$transaction(updates);
|
|
||||||
|
|
||||||
return this.prisma.scriptSegment.findMany({
|
|
||||||
where: { projectId },
|
|
||||||
orderBy: { sortOrder: 'asc' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a full script for a project
|
|
||||||
*
|
|
||||||
* @param projectId - Project ID
|
|
||||||
* @returns Generated segments
|
|
||||||
*/
|
|
||||||
async generateScript(projectId: string) {
|
|
||||||
this.logger.log(`Generating script for project: ${projectId}`);
|
|
||||||
|
|
||||||
// Auto-snapshot current state before regeneration
|
|
||||||
await this.versionsService.createSnapshot(
|
|
||||||
projectId,
|
|
||||||
'AUTO_SAVE',
|
|
||||||
undefined,
|
|
||||||
'Auto-save before script generation',
|
|
||||||
).catch(() => { /* ignore if no segments yet */ });
|
|
||||||
|
|
||||||
// Update status
|
|
||||||
await this.prisma.scriptProject.update({
|
|
||||||
where: { id: projectId },
|
|
||||||
data: { status: 'SCRIPTING' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const project = await this.prisma.scriptProject.findUnique({
|
|
||||||
where: { id: projectId },
|
|
||||||
include: {
|
|
||||||
sources: { where: { selected: true } },
|
|
||||||
briefItems: { orderBy: { sortOrder: 'asc' } },
|
|
||||||
characters: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build context from project data
|
|
||||||
const sourceContext = project.sources
|
|
||||||
.slice(0, 5)
|
|
||||||
.map((s, i) => `[Source ${i + 1}] (${s.type}): ${s.title} - ${s.snippet}`)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const briefContext = project.briefItems
|
|
||||||
.map((b) => `Q: ${b.question}\nA: ${b.answer}`)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const characterContext = project.characters
|
|
||||||
.map(
|
|
||||||
(c) =>
|
|
||||||
`${c.name} (${c.role}): Values[${c.values}] Traits[${c.traits}] Mannerisms[${c.mannerisms}]`,
|
|
||||||
)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
// Calculate target metrics
|
|
||||||
const targetWordCount = calculateTargetWordCount(project.targetDuration);
|
|
||||||
const estimatedChapters = calculateEstimatedChapters(targetWordCount);
|
|
||||||
|
|
||||||
// PHASE 1: Generate Outline using prompt builder
|
|
||||||
const outlinePromptData = buildScriptOutlinePrompt({
|
|
||||||
topic: project.topic,
|
|
||||||
logline: project.logline || '',
|
|
||||||
characterContext,
|
|
||||||
speechStyles: project.speechStyle,
|
|
||||||
targetAudience: project.targetAudience,
|
|
||||||
contentType: project.contentType,
|
|
||||||
targetDuration: project.targetDuration,
|
|
||||||
targetWordCount,
|
|
||||||
estimatedChapters,
|
|
||||||
sourceContext,
|
|
||||||
briefContext,
|
|
||||||
});
|
|
||||||
|
|
||||||
const outlineResp = await this.gemini.generateJSON<{
|
|
||||||
title: string;
|
|
||||||
seoDescription: string;
|
|
||||||
tags: string[];
|
|
||||||
thumbnailIdeas: string[];
|
|
||||||
chapters: { title: string; focus: string; type: string }[];
|
|
||||||
}>(outlinePromptData.prompt, outlinePromptData.schema, {
|
|
||||||
temperature: outlinePromptData.temperature,
|
|
||||||
});
|
|
||||||
|
|
||||||
const outlineData = outlineResp.data;
|
|
||||||
|
|
||||||
// Update project with SEO data
|
|
||||||
await this.prisma.scriptProject.update({
|
|
||||||
where: { id: projectId },
|
|
||||||
data: {
|
|
||||||
seoTitle: outlineData.title,
|
|
||||||
seoDescription: outlineData.seoDescription,
|
|
||||||
seoTags: outlineData.tags,
|
|
||||||
thumbnailIdeas: outlineData.thumbnailIdeas,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// PHASE 2: Generate each chapter using prompt builder
|
|
||||||
const generatedSegments: any[] = [];
|
|
||||||
let timeOffset = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < outlineData.chapters.length; i++) {
|
|
||||||
const chapter = outlineData.chapters[i];
|
|
||||||
|
|
||||||
const chapterPromptData = buildChapterSegmentPrompt({
|
|
||||||
chapterIndex: i,
|
|
||||||
totalChapters: outlineData.chapters.length,
|
|
||||||
chapterTitle: chapter.title,
|
|
||||||
chapterFocus: chapter.focus,
|
|
||||||
chapterType: chapter.type,
|
|
||||||
speechStyles: project.speechStyle,
|
|
||||||
targetAudience: project.targetAudience,
|
|
||||||
characterContext,
|
|
||||||
language: project.language,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const segmentResp = await this.gemini.generateJSON<any[]>(
|
|
||||||
chapterPromptData.prompt,
|
|
||||||
chapterPromptData.schema,
|
|
||||||
{ temperature: chapterPromptData.temperature },
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const seg of segmentResp.data) {
|
|
||||||
const words = seg.narratorScript
|
|
||||||
? seg.narratorScript.split(' ').length
|
|
||||||
: 0;
|
|
||||||
const dur = Math.max(5, Math.ceil(words / (140 / 60)));
|
|
||||||
const start = this.formatTime(timeOffset);
|
|
||||||
timeOffset += dur;
|
|
||||||
|
|
||||||
generatedSegments.push({
|
|
||||||
projectId,
|
|
||||||
segmentType: seg.segmentType || 'Body',
|
|
||||||
timeStart: start,
|
|
||||||
duration: `${dur}s`,
|
|
||||||
narratorScript: seg.narratorScript || '',
|
|
||||||
visualDescription: seg.visualDescription || 'Background',
|
|
||||||
videoPrompt:
|
|
||||||
seg.videoPrompt || `Cinematic shot of ${seg.stockQuery}`,
|
|
||||||
imagePrompt:
|
|
||||||
seg.imagePrompt || `High quality image of ${seg.stockQuery}`,
|
|
||||||
onScreenText: seg.onScreenText || '',
|
|
||||||
editorNotes: '',
|
|
||||||
generalNotes: '',
|
|
||||||
audioCues: seg.audioCues || '',
|
|
||||||
stockQuery: seg.stockQuery || 'background',
|
|
||||||
sortOrder: generatedSegments.length,
|
|
||||||
citationIndexes: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn(`Failed to generate chapter ${i + 1}: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear existing segments and insert new ones
|
|
||||||
await this.prisma.scriptSegment.deleteMany({ where: { projectId } });
|
|
||||||
|
|
||||||
if (generatedSegments.length > 0) {
|
|
||||||
await this.prisma.scriptSegment.createMany({
|
|
||||||
data: generatedSegments,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.prisma.scriptSegment.findMany({
|
|
||||||
where: { projectId },
|
|
||||||
orderBy: { sortOrder: 'asc' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rewrite a segment with a new style
|
|
||||||
*
|
|
||||||
* @param segmentId - Segment ID
|
|
||||||
* @param newStyle - New speech style
|
|
||||||
* @returns Updated segment
|
|
||||||
*/
|
|
||||||
async rewriteSegment(
|
|
||||||
segmentId: string,
|
|
||||||
newStyle: string, // SpeechStyle or 'Make it Longer' | 'Make it Shorter'
|
|
||||||
) {
|
|
||||||
const segment = await this.prisma.scriptSegment.findUnique({
|
|
||||||
where: { id: segmentId },
|
|
||||||
include: { project: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!segment) {
|
|
||||||
throw new NotFoundException(`Segment with ID ${segmentId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const promptData = buildSegmentRewritePrompt({
|
|
||||||
currentScript: segment.narratorScript || '',
|
|
||||||
newStyle,
|
|
||||||
topic: segment.project.topic,
|
|
||||||
language: segment.project.language,
|
|
||||||
});
|
|
||||||
|
|
||||||
const rewriteResp = await this.gemini.generateJSON<{
|
|
||||||
narratorScript: string;
|
|
||||||
visualDescription: string;
|
|
||||||
onScreenText: string;
|
|
||||||
audioCues: string;
|
|
||||||
}>(promptData.prompt, promptData.schema, {
|
|
||||||
temperature: promptData.temperature,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = rewriteResp.data;
|
|
||||||
const words = data.narratorScript
|
|
||||||
? data.narratorScript.split(' ').length
|
|
||||||
: 0;
|
|
||||||
const dur = Math.max(5, Math.ceil(words / (140 / 60)));
|
|
||||||
|
|
||||||
return this.prisma.scriptSegment.update({
|
|
||||||
where: { id: segmentId },
|
|
||||||
data: {
|
|
||||||
narratorScript: data.narratorScript,
|
|
||||||
visualDescription: data.visualDescription,
|
|
||||||
onScreenText: data.onScreenText,
|
|
||||||
audioCues: data.audioCues,
|
|
||||||
duration: `${dur}s`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format seconds to MM:SS
|
|
||||||
*/
|
|
||||||
private formatTime(seconds: number): string {
|
|
||||||
const m = Math.floor(seconds / 60);
|
|
||||||
const s = seconds % 60;
|
|
||||||
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate an image for a specific segment
|
|
||||||
*/
|
|
||||||
async generateSegmentImage(segmentId: string) {
|
|
||||||
const segment = await this.prisma.scriptSegment.findUnique({
|
|
||||||
where: { id: segmentId },
|
|
||||||
include: { project: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!segment) {
|
|
||||||
throw new NotFoundException(`Segment with ID ${segmentId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Generate/Refine Image Prompt using LLM
|
|
||||||
const promptData = buildSegmentImagePrompt({
|
|
||||||
topic: segment.project.topic,
|
|
||||||
narratorScript: segment.narratorScript || '',
|
|
||||||
visualDescription: segment.visualDescription || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const prompts = await this.gemini.generateJSON<{
|
|
||||||
imagePrompt: string;
|
|
||||||
videoPrompt: string;
|
|
||||||
}>(promptData.prompt, promptData.schema, {
|
|
||||||
temperature: promptData.temperature,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Use the new image prompt for generation
|
|
||||||
const imageUrl = await this.analysisService.generateThumbnailImage(
|
|
||||||
prompts.data.imagePrompt,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Update segment with new prompts AND generated image URL
|
|
||||||
return this.prisma.scriptSegment.update({
|
|
||||||
where: { id: segmentId },
|
|
||||||
data: {
|
|
||||||
imagePrompt: prompts.data.imagePrompt,
|
|
||||||
videoPrompt: prompts.data.videoPrompt,
|
|
||||||
generatedImageUrl: imageUrl,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== REGENERATE (Tek segment / Partial) ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Regenerate a single segment with AI.
|
|
||||||
* Auto-snapshots current state before regeneration.
|
|
||||||
*
|
|
||||||
* @param segmentId - Segment ID to regenerate
|
|
||||||
* @returns Updated segment
|
|
||||||
*/
|
|
||||||
async regenerateSegment(segmentId: string) {
|
|
||||||
const segment = await this.prisma.scriptSegment.findUnique({
|
|
||||||
where: { id: segmentId },
|
|
||||||
include: { project: { include: { characters: true } } },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!segment) {
|
|
||||||
throw new NotFoundException(`Segment with ID ${segmentId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-snapshot before regeneration
|
|
||||||
await this.versionsService.createSnapshot(
|
|
||||||
segment.projectId,
|
|
||||||
'AUTO_SAVE',
|
|
||||||
undefined,
|
|
||||||
`Auto-save before regenerating segment #${segment.sortOrder + 1}`,
|
|
||||||
).catch(() => {});
|
|
||||||
|
|
||||||
const characterContext = segment.project.characters
|
|
||||||
.map((c) => `${c.name} (${c.role}): Values[${c.values}] Traits[${c.traits}]`)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const chapterPromptData = buildChapterSegmentPrompt({
|
|
||||||
chapterIndex: segment.sortOrder,
|
|
||||||
totalChapters: 1,
|
|
||||||
chapterTitle: segment.segmentType,
|
|
||||||
chapterFocus: segment.visualDescription || segment.segmentType,
|
|
||||||
chapterType: segment.segmentType,
|
|
||||||
speechStyles: segment.project.speechStyle,
|
|
||||||
targetAudience: segment.project.targetAudience,
|
|
||||||
characterContext,
|
|
||||||
language: segment.project.language,
|
|
||||||
});
|
|
||||||
|
|
||||||
const resp = await this.gemini.generateJSON<any[]>(
|
|
||||||
chapterPromptData.prompt,
|
|
||||||
chapterPromptData.schema,
|
|
||||||
{ temperature: chapterPromptData.temperature },
|
|
||||||
);
|
|
||||||
|
|
||||||
const newSeg = resp.data[0];
|
|
||||||
if (!newSeg) return segment;
|
|
||||||
|
|
||||||
const words = newSeg.narratorScript ? newSeg.narratorScript.split(' ').length : 0;
|
|
||||||
const dur = Math.max(5, Math.ceil(words / (140 / 60)));
|
|
||||||
|
|
||||||
return this.prisma.scriptSegment.update({
|
|
||||||
where: { id: segmentId },
|
|
||||||
data: {
|
|
||||||
narratorScript: newSeg.narratorScript,
|
|
||||||
visualDescription: newSeg.visualDescription,
|
|
||||||
videoPrompt: newSeg.videoPrompt,
|
|
||||||
imagePrompt: newSeg.imagePrompt,
|
|
||||||
onScreenText: newSeg.onScreenText,
|
|
||||||
audioCues: newSeg.audioCues,
|
|
||||||
stockQuery: newSeg.stockQuery,
|
|
||||||
duration: `${dur}s`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Regenerate multiple selected segments.
|
|
||||||
*
|
|
||||||
* @param projectId - Project ID
|
|
||||||
* @param segmentIds - Array of segment IDs to regenerate
|
|
||||||
* @returns Updated segments
|
|
||||||
*/
|
|
||||||
async regeneratePartial(projectId: string, segmentIds: string[]) {
|
|
||||||
// Auto-snapshot
|
|
||||||
await this.versionsService.createSnapshot(
|
|
||||||
projectId,
|
|
||||||
'AUTO_SAVE',
|
|
||||||
undefined,
|
|
||||||
`Auto-save before partial regeneration (${segmentIds.length} segments)`,
|
|
||||||
).catch(() => {});
|
|
||||||
|
|
||||||
const results: any[] = [];
|
|
||||||
for (const segId of segmentIds) {
|
|
||||||
try {
|
|
||||||
const result = await this.regenerateSegment(segId);
|
|
||||||
results.push(result);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn(`Failed to regenerate segment ${segId}: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,382 +0,0 @@
|
|||||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
||||||
import { PrismaService } from '../../../database/prisma.service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VersionsService
|
|
||||||
*
|
|
||||||
* Manages script version history — automatic snapshots, manual saves,
|
|
||||||
* version restoration, and diff comparison.
|
|
||||||
*
|
|
||||||
* TR: Script versiyon geçmişi yönetimi — otomatik snapshot, manuel kayıt,
|
|
||||||
* versiyona geri dönüş ve karşılaştırma.
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class VersionsService {
|
|
||||||
private readonly logger = new Logger(VersionsService.name);
|
|
||||||
|
|
||||||
/** Maximum number of versions to keep per project */
|
|
||||||
private readonly MAX_VERSIONS = 20;
|
|
||||||
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
|
||||||
|
|
||||||
// ========== VERSION LISTING ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List all versions for a project
|
|
||||||
*/
|
|
||||||
async listVersions(projectId: string) {
|
|
||||||
await this.ensureProjectExists(projectId);
|
|
||||||
|
|
||||||
return this.prisma.scriptVersion.findMany({
|
|
||||||
where: { projectId },
|
|
||||||
orderBy: { versionNumber: 'desc' },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
versionNumber: true,
|
|
||||||
label: true,
|
|
||||||
generatedBy: true,
|
|
||||||
segmentCount: true,
|
|
||||||
totalWords: true,
|
|
||||||
changeNote: true,
|
|
||||||
createdAt: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a specific version with full snapshot data
|
|
||||||
*/
|
|
||||||
async getVersion(projectId: string, versionId: string) {
|
|
||||||
const version = await this.prisma.scriptVersion.findFirst({
|
|
||||||
where: { id: versionId, projectId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!version) {
|
|
||||||
throw new NotFoundException(
|
|
||||||
`Version ${versionId} not found for project ${projectId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== VERSION CREATION ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a snapshot of the current script state.
|
|
||||||
* Called automatically before AI generation or manually by user.
|
|
||||||
*
|
|
||||||
* @param projectId - Project ID
|
|
||||||
* @param generatedBy - Who created this version: 'AI' | 'USER' | 'AUTO_SAVE'
|
|
||||||
* @param label - Optional label for the version
|
|
||||||
* @param changeNote - Optional note about what changed
|
|
||||||
* @returns Created version
|
|
||||||
*/
|
|
||||||
async createSnapshot(
|
|
||||||
projectId: string,
|
|
||||||
generatedBy: string = 'AUTO_SAVE',
|
|
||||||
label?: string,
|
|
||||||
changeNote?: string,
|
|
||||||
) {
|
|
||||||
const project = await this.prisma.scriptProject.findUnique({
|
|
||||||
where: { id: projectId },
|
|
||||||
include: {
|
|
||||||
segments: { orderBy: { sortOrder: 'asc' } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't create empty snapshots
|
|
||||||
if (!project.segments || project.segments.length === 0) {
|
|
||||||
this.logger.warn(
|
|
||||||
`No segments to snapshot for project ${projectId}. Skipping.`,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate next version number
|
|
||||||
const nextVersion = project.currentVersionNumber + 1;
|
|
||||||
|
|
||||||
// Build snapshot data (strip relation fields, keep only data)
|
|
||||||
const snapshotData = project.segments.map((seg) => ({
|
|
||||||
segmentType: seg.segmentType,
|
|
||||||
timeStart: seg.timeStart,
|
|
||||||
duration: seg.duration,
|
|
||||||
narratorScript: seg.narratorScript,
|
|
||||||
visualDescription: seg.visualDescription,
|
|
||||||
videoPrompt: seg.videoPrompt,
|
|
||||||
imagePrompt: seg.imagePrompt,
|
|
||||||
onScreenText: seg.onScreenText,
|
|
||||||
editorNotes: seg.editorNotes,
|
|
||||||
generalNotes: seg.generalNotes,
|
|
||||||
audioCues: seg.audioCues,
|
|
||||||
stockQuery: seg.stockQuery,
|
|
||||||
generatedImageUrl: seg.generatedImageUrl,
|
|
||||||
sortOrder: seg.sortOrder,
|
|
||||||
citationIndexes: seg.citationIndexes,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Build SEO snapshot
|
|
||||||
const seoSnapshot = {
|
|
||||||
seoTitle: project.seoTitle,
|
|
||||||
seoDescription: project.seoDescription,
|
|
||||||
seoTags: project.seoTags,
|
|
||||||
thumbnailIdeas: project.thumbnailIdeas,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate total words
|
|
||||||
const totalWords = project.segments.reduce((acc, seg) => {
|
|
||||||
return acc + (seg.narratorScript?.split(' ').length || 0);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// Create version
|
|
||||||
const version = await this.prisma.scriptVersion.create({
|
|
||||||
data: {
|
|
||||||
projectId,
|
|
||||||
versionNumber: nextVersion,
|
|
||||||
label:
|
|
||||||
label ||
|
|
||||||
`v${nextVersion} — ${generatedBy === 'AI' ? 'AI Generated' : generatedBy === 'USER' ? 'Manual Save' : 'Auto Save'}`,
|
|
||||||
generatedBy,
|
|
||||||
snapshotData: snapshotData as any,
|
|
||||||
seoSnapshot: seoSnapshot as any,
|
|
||||||
segmentCount: project.segments.length,
|
|
||||||
totalWords,
|
|
||||||
changeNote,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update project's current version number
|
|
||||||
await this.prisma.scriptProject.update({
|
|
||||||
where: { id: projectId },
|
|
||||||
data: { currentVersionNumber: nextVersion },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cleanup old versions (keep only MAX_VERSIONS)
|
|
||||||
await this.cleanupOldVersions(projectId);
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Created version v${nextVersion} for project ${projectId} (${generatedBy})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== VERSION RESTORE ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore a project to a specific version.
|
|
||||||
* Creates a snapshot of current state before restoring.
|
|
||||||
*
|
|
||||||
* @param projectId - Project ID
|
|
||||||
* @param versionId - Version ID to restore to
|
|
||||||
* @returns Restored segments
|
|
||||||
*/
|
|
||||||
async restoreVersion(projectId: string, versionId: string) {
|
|
||||||
const version = await this.getVersion(projectId, versionId);
|
|
||||||
|
|
||||||
// First, snapshot current state before restoring
|
|
||||||
await this.createSnapshot(
|
|
||||||
projectId,
|
|
||||||
'AUTO_SAVE',
|
|
||||||
undefined,
|
|
||||||
`Auto-save before restoring to v${version.versionNumber}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Parse snapshot data
|
|
||||||
const segments = version.snapshotData as any[];
|
|
||||||
|
|
||||||
if (!segments || segments.length === 0) {
|
|
||||||
throw new Error('Version snapshot has no segment data');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete current segments
|
|
||||||
await this.prisma.scriptSegment.deleteMany({ where: { projectId } });
|
|
||||||
|
|
||||||
// Restore segments from snapshot
|
|
||||||
await this.prisma.scriptSegment.createMany({
|
|
||||||
data: segments.map((seg) => ({
|
|
||||||
projectId,
|
|
||||||
segmentType: seg.segmentType || 'Body',
|
|
||||||
timeStart: seg.timeStart || '00:00',
|
|
||||||
duration: seg.duration || '0s',
|
|
||||||
narratorScript: seg.narratorScript,
|
|
||||||
visualDescription: seg.visualDescription,
|
|
||||||
videoPrompt: seg.videoPrompt,
|
|
||||||
imagePrompt: seg.imagePrompt,
|
|
||||||
onScreenText: seg.onScreenText,
|
|
||||||
editorNotes: seg.editorNotes,
|
|
||||||
generalNotes: seg.generalNotes,
|
|
||||||
audioCues: seg.audioCues,
|
|
||||||
stockQuery: seg.stockQuery,
|
|
||||||
generatedImageUrl: seg.generatedImageUrl,
|
|
||||||
sortOrder: seg.sortOrder ?? 0,
|
|
||||||
citationIndexes: seg.citationIndexes || [],
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restore SEO data if present
|
|
||||||
const seoData = version.seoSnapshot as any;
|
|
||||||
if (seoData) {
|
|
||||||
await this.prisma.scriptProject.update({
|
|
||||||
where: { id: projectId },
|
|
||||||
data: {
|
|
||||||
seoTitle: seoData.seoTitle,
|
|
||||||
seoDescription: seoData.seoDescription,
|
|
||||||
seoTags: seoData.seoTags || [],
|
|
||||||
thumbnailIdeas: seoData.thumbnailIdeas || [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Restored project ${projectId} to version v${version.versionNumber}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Return the restored project data
|
|
||||||
return this.prisma.scriptProject.findUnique({
|
|
||||||
where: { id: projectId },
|
|
||||||
include: {
|
|
||||||
segments: { orderBy: { sortOrder: 'asc' } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== VERSION DELETION ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a specific version
|
|
||||||
*/
|
|
||||||
async deleteVersion(projectId: string, versionId: string) {
|
|
||||||
const version = await this.getVersion(projectId, versionId);
|
|
||||||
|
|
||||||
await this.prisma.scriptVersion.delete({
|
|
||||||
where: { id: version.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
return { deleted: true, versionNumber: version.versionNumber };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== VERSION COMPARISON ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare two versions and return differences
|
|
||||||
*/
|
|
||||||
async compareVersions(
|
|
||||||
projectId: string,
|
|
||||||
versionAId: string,
|
|
||||||
versionBId: string,
|
|
||||||
) {
|
|
||||||
const [versionA, versionB] = await Promise.all([
|
|
||||||
this.getVersion(projectId, versionAId),
|
|
||||||
this.getVersion(projectId, versionBId),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const segmentsA = (versionA.snapshotData as any[]) || [];
|
|
||||||
const segmentsB = (versionB.snapshotData as any[]) || [];
|
|
||||||
|
|
||||||
const maxLen = Math.max(segmentsA.length, segmentsB.length);
|
|
||||||
const diffs: any[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < maxLen; i++) {
|
|
||||||
const segA = segmentsA[i] || null;
|
|
||||||
const segB = segmentsB[i] || null;
|
|
||||||
|
|
||||||
if (!segA && segB) {
|
|
||||||
diffs.push({
|
|
||||||
index: i,
|
|
||||||
type: 'added',
|
|
||||||
segmentType: segB.segmentType,
|
|
||||||
narratorScript: { before: null, after: segB.narratorScript },
|
|
||||||
});
|
|
||||||
} else if (segA && !segB) {
|
|
||||||
diffs.push({
|
|
||||||
index: i,
|
|
||||||
type: 'removed',
|
|
||||||
segmentType: segA.segmentType,
|
|
||||||
narratorScript: { before: segA.narratorScript, after: null },
|
|
||||||
});
|
|
||||||
} else if (segA && segB) {
|
|
||||||
const changed =
|
|
||||||
segA.narratorScript !== segB.narratorScript ||
|
|
||||||
segA.visualDescription !== segB.visualDescription ||
|
|
||||||
segA.onScreenText !== segB.onScreenText;
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
diffs.push({
|
|
||||||
index: i,
|
|
||||||
type: 'modified',
|
|
||||||
segmentType: segB.segmentType,
|
|
||||||
narratorScript: {
|
|
||||||
before: segA.narratorScript,
|
|
||||||
after: segB.narratorScript,
|
|
||||||
},
|
|
||||||
visualDescription: {
|
|
||||||
before: segA.visualDescription,
|
|
||||||
after: segB.visualDescription,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
versionA: {
|
|
||||||
id: versionA.id,
|
|
||||||
versionNumber: versionA.versionNumber,
|
|
||||||
label: versionA.label,
|
|
||||||
createdAt: versionA.createdAt,
|
|
||||||
},
|
|
||||||
versionB: {
|
|
||||||
id: versionB.id,
|
|
||||||
versionNumber: versionB.versionNumber,
|
|
||||||
label: versionB.label,
|
|
||||||
createdAt: versionB.createdAt,
|
|
||||||
},
|
|
||||||
totalDiffs: diffs.length,
|
|
||||||
diffs,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== HELPERS ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify project exists or throw
|
|
||||||
*/
|
|
||||||
private async ensureProjectExists(projectId: string) {
|
|
||||||
const exists = await this.prisma.scriptProject.findUnique({
|
|
||||||
where: { id: projectId },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
if (!exists) {
|
|
||||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove old versions beyond the max limit per project
|
|
||||||
*/
|
|
||||||
private async cleanupOldVersions(projectId: string) {
|
|
||||||
const versions = await this.prisma.scriptVersion.findMany({
|
|
||||||
where: { projectId },
|
|
||||||
orderBy: { versionNumber: 'desc' },
|
|
||||||
select: { id: true, versionNumber: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (versions.length > this.MAX_VERSIONS) {
|
|
||||||
const toDelete = versions.slice(this.MAX_VERSIONS);
|
|
||||||
await this.prisma.scriptVersion.deleteMany({
|
|
||||||
where: {
|
|
||||||
id: { in: toDelete.map((v) => v.id) },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Cleaned up ${toDelete.length} old versions for project ${projectId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { DatabaseModule } from '../../database/database.module';
|
|
||||||
import { GeminiModule } from '../gemini/gemini.module';
|
|
||||||
|
|
||||||
// Controllers
|
|
||||||
import {
|
|
||||||
ProjectsController,
|
|
||||||
ScriptsController,
|
|
||||||
ResearchController,
|
|
||||||
AnalysisController,
|
|
||||||
VersionsController,
|
|
||||||
} from './controllers';
|
|
||||||
|
|
||||||
// Services
|
|
||||||
import {
|
|
||||||
ProjectsService,
|
|
||||||
ScriptsService,
|
|
||||||
ResearchService,
|
|
||||||
AnalysisService,
|
|
||||||
VersionsService,
|
|
||||||
} from './services';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SkriptAI Module
|
|
||||||
*
|
|
||||||
* Main module for the SkriptAI feature - AI-powered video script generation.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Project management (CRUD, duplicate, export)
|
|
||||||
* - Script generation and editing
|
|
||||||
* - Deep research with AI
|
|
||||||
* - Neuro Marketing analysis
|
|
||||||
* - YouTube audit
|
|
||||||
* - Commercial brief generation
|
|
||||||
* - Version history & content management
|
|
||||||
*
|
|
||||||
* TR: SkriptAI ana modülü - AI destekli video script üretimi.
|
|
||||||
* EN: Main module for the SkriptAI feature - AI-powered video script generation.
|
|
||||||
*/
|
|
||||||
@Module({
|
|
||||||
imports: [DatabaseModule, GeminiModule],
|
|
||||||
controllers: [
|
|
||||||
ProjectsController,
|
|
||||||
ScriptsController,
|
|
||||||
ResearchController,
|
|
||||||
AnalysisController,
|
|
||||||
VersionsController,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
ProjectsService,
|
|
||||||
ScriptsService,
|
|
||||||
ResearchService,
|
|
||||||
AnalysisService,
|
|
||||||
VersionsService,
|
|
||||||
],
|
|
||||||
exports: [
|
|
||||||
ProjectsService,
|
|
||||||
ScriptsService,
|
|
||||||
ResearchService,
|
|
||||||
AnalysisService,
|
|
||||||
VersionsService,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class SkriptaiModule {}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
/**
|
|
||||||
* SkriptAI Types
|
|
||||||
*
|
|
||||||
* Type definitions for the SkriptAI module.
|
|
||||||
* These mirror the original TypeScript types from the legacy app.
|
|
||||||
*
|
|
||||||
* TR: SkriptAI modülü için tip tanımlamaları.
|
|
||||||
* EN: Type definitions for the SkriptAI module.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Content type definitions
|
|
||||||
export type ContentFormat =
|
|
||||||
| 'YouTube Documentary'
|
|
||||||
| 'YouTube Long Form (Edu/Video Essay)'
|
|
||||||
| 'YouTube Short / TikTok'
|
|
||||||
| 'Kids Cartoon (Script & Dialogue)'
|
|
||||||
| 'Preschool Learning (Slow Paced)'
|
|
||||||
| 'True Crime Story'
|
|
||||||
| 'Product Showcase / Ad'
|
|
||||||
| 'Corporate Presentation'
|
|
||||||
| 'Newsletter / Blog Post'
|
|
||||||
| 'News Bulletin / Journalism';
|
|
||||||
|
|
||||||
export type TargetAudience =
|
|
||||||
| 'Preschool (0-5 Years)'
|
|
||||||
| 'Kids (6-12 Years)'
|
|
||||||
| 'Teenagers (13-17 Years)'
|
|
||||||
| 'Young Adults (18-24 Years)'
|
|
||||||
| 'Adults (25-45 Years)'
|
|
||||||
| 'Seniors (60+ Years)'
|
|
||||||
| 'Professionals / B2B'
|
|
||||||
| 'Mature (18+) / Uncensored'
|
|
||||||
| 'General Audience';
|
|
||||||
|
|
||||||
export type SpeechStyle =
|
|
||||||
| 'Standard / Balanced'
|
|
||||||
| 'Casual / Conversational'
|
|
||||||
| 'Street / Slang (Argo)'
|
|
||||||
| 'Formal / Corporate'
|
|
||||||
| 'Poetic / Artistic'
|
|
||||||
| 'Humorous / Witty'
|
|
||||||
| 'Dramatic / Intense'
|
|
||||||
| 'Tech-Savvy / Jargon'
|
|
||||||
| 'Storyteller / Narrator'
|
|
||||||
| 'Fairy Tale / Masal'
|
|
||||||
| 'Didactic / Educational'
|
|
||||||
| 'Dark / Noir / Mystery'
|
|
||||||
| 'Satirical / Sarcastic'
|
|
||||||
| 'Motivational / High Energy';
|
|
||||||
|
|
||||||
export type SegmentType =
|
|
||||||
| 'Hook'
|
|
||||||
| 'Intro'
|
|
||||||
| 'Body'
|
|
||||||
| 'Ad/Sponsor'
|
|
||||||
| 'CTA'
|
|
||||||
| 'Outro'
|
|
||||||
| 'Scene'
|
|
||||||
| 'Dialogue'
|
|
||||||
| 'Section'
|
|
||||||
| 'Headline';
|
|
||||||
|
|
||||||
export type SourceType =
|
|
||||||
| 'article'
|
|
||||||
| 'video'
|
|
||||||
| 'interview'
|
|
||||||
| 'academic'
|
|
||||||
| 'book'
|
|
||||||
| 'document';
|
|
||||||
|
|
||||||
export type CharacterRole =
|
|
||||||
| 'Protagonist'
|
|
||||||
| 'Antagonist'
|
|
||||||
| 'Guide/Mentor'
|
|
||||||
| 'Sidekick'
|
|
||||||
| 'Narrator';
|
|
||||||
|
|
||||||
// Neuro Marketing Analysis Result
|
|
||||||
export interface NeuroAnalysisResult {
|
|
||||||
engagementScore: number;
|
|
||||||
dopamineScore: number;
|
|
||||||
clarityScore: number;
|
|
||||||
persuasionMetrics: {
|
|
||||||
reciprocity: number;
|
|
||||||
scarcity: number;
|
|
||||||
authority: number;
|
|
||||||
consistency: number;
|
|
||||||
liking: number;
|
|
||||||
socialProof: number;
|
|
||||||
};
|
|
||||||
neuroMetrics: {
|
|
||||||
attentionHooks: string[];
|
|
||||||
emotionalTriggers: string[];
|
|
||||||
memoryAnchors: string[];
|
|
||||||
actionDrivers: string[];
|
|
||||||
};
|
|
||||||
suggestions: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// YouTube Audit Result
|
|
||||||
export interface RetentionPoint {
|
|
||||||
time: string;
|
|
||||||
issue: string;
|
|
||||||
suggestion: string;
|
|
||||||
severity: 'High' | 'Medium' | 'Low';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ThumbnailConcept {
|
|
||||||
conceptName: string;
|
|
||||||
visualDescription: string;
|
|
||||||
textOverlay: string;
|
|
||||||
colorPsychology: string;
|
|
||||||
emotionTarget: string;
|
|
||||||
aiPrompt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface YoutubeAudit {
|
|
||||||
hookScore: number;
|
|
||||||
pacingScore: number;
|
|
||||||
viralPotential: number;
|
|
||||||
retentionAnalysis: RetentionPoint[];
|
|
||||||
thumbnails: ThumbnailConcept[];
|
|
||||||
titles: string[];
|
|
||||||
communityPost: string;
|
|
||||||
pinnedComment: string;
|
|
||||||
description?: string;
|
|
||||||
keywords?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post Production Brief
|
|
||||||
export interface PostProductionBrief {
|
|
||||||
overview: string;
|
|
||||||
pacing: string;
|
|
||||||
colorGrade: string;
|
|
||||||
musicStyle: string;
|
|
||||||
timeline: { time: string; action: string; asset: string }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commercial Brief
|
|
||||||
export interface Sponsor {
|
|
||||||
name: string;
|
|
||||||
industry: string;
|
|
||||||
matchReason: string;
|
|
||||||
emailDraft: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CommercialBrief {
|
|
||||||
viabilityScore: string;
|
|
||||||
viabilityReason: string;
|
|
||||||
sponsors: Sponsor[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// AI Config
|
|
||||||
export const AI_CONFIG = {
|
|
||||||
PRIMARY: 'gemini-2.5-flash',
|
|
||||||
FAST: 'gemini-2.0-flash',
|
|
||||||
} as const;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
NEXTAUTH_URL=http://skript.bilgich.com
|
|
||||||
NEXTAUTH_SECRET=skriptai_prod_secret_998877665544332211
|
|
||||||
NEXT_PUBLIC_API_URL=/api
|
|
||||||
NEXT_PUBLIC_AUTH_REQUIRED=false
|
|
||||||
NEXT_PUBLIC_GOOGLE_API_KEY=AIzaSyBnrLw5W4NidP4w-70x59fcPolz0izMVfU
|
|
||||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -22,4 +22,4 @@
|
|||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"noFallthroughCasesInSwitch": false
|
"noFallthroughCasesInSwitch": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user