Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ OLLAMA_PORT=""
GEMINI_API_KEY=""
USE_GEMINI="false"

# MiniMax Option
MINIMAX_API_KEY=""
USE_MINIMAX="false"
MINIMAX_MODEL="MiniMax-M1"

# Ports Mapping
CHROMA_PORT=8000
PORT=3745
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Note: (Edit Mind name is coming from Video Editor Mind, so this will be the edit
| **Containerization** | [Docker](https://www.docker.com/), [Docker Compose](https://docs.docker.com/compose/) |
| **Web Service** | [React Router V7](https://reactrouter.com/), [TypeScript](https://www.typescriptlang.org/), [Vite](https://vitejs.dev/) |
| **Background Jobs Service** | [Node.js](https://nodejs.org/), [Express.js](https://expressjs.com/), [BullMQ](https://bullmq.io/) |
| **ML Sevice** | [Python](https://www.python.org/), [PyAV](https://github.com/PyAV-Org/PyAV), [PyTorch](https://pytorch.org/), OpenAI Whisper, Google Gemini or Ollama (Used for NLP) |
| **ML Sevice** | [Python](https://www.python.org/), [PyAV](https://github.com/PyAV-Org/PyAV), [PyTorch](https://pytorch.org/), OpenAI Whisper, Google Gemini, [MiniMax](https://www.minimaxi.com/) or Ollama (Used for NLP) |
| **Vector Database** | [ChromaDB](https://www.trychroma.com/) |
| **Relational DB** | [PostgreSQL](https://www.postgresql.org/) (via [Prisma ORM](https://www.prisma.io/)) |

Expand Down Expand Up @@ -132,6 +132,11 @@ OLLAMA_MODEL="qwen2.5:7b-instruct"
USE_GEMINI="true"
GEMINI_API_KEY="your-gemini-api-key-from-google-ai-studio"

# Option C: Use MiniMax API (requires API key)
USE_MINIMAX="true"
MINIMAX_API_KEY="your-minimax-api-key"
MINIMAX_MODEL="MiniMax-M1" # or MiniMax-M1-highspeed

# 3. GENERATE SECURITY KEYS (REQUIRED)
# Generate with: openssl rand -base64 32
ENCRYPTION_KEY="your-random-32-char-base64-key"
Expand Down
1 change: 1 addition & 0 deletions packages/ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@google/generative-ai": "^0.24.1",
"db": "workspace:*",
"ollama": "^0.6.3",
"openai": "^4.104.0",
"search": "workspace:*",
"shared": "workspace:*"
}
Expand Down
5 changes: 5 additions & 0 deletions packages/ai/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ export const USE_OLLAMA_MODEL = process.env.USE_OLLAMA_MODEL === 'true'
export const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'qwen2.5:7b-instruct'
export const OLLAMA_HOST = process.env.OLLAMA_HOST
export const OLLAMA_PORT = process.env.OLLAMA_PORT

// MiniMax Settings
export const MINIMAX_API_KEY = process.env.MINIMAX_API_KEY
export const USE_MINIMAX = process.env.USE_MINIMAX === 'true'
export const MINIMAX_MODEL = process.env.MINIMAX_MODEL || 'MiniMax-M1'
229 changes: 229 additions & 0 deletions packages/ai/src/services/minimax.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import OpenAI from 'openai'
import type { ChatMessage } from '@prisma/client'
import {
SEARCH_PROMPT,
ASSISTANT_MESSAGE_PROMPT,
VIDEO_COMPILATION_MESSAGE_PROMPT,
YEAR_IN_REVIEW,
GENERAL_RESPONSE_PROMPT,
CLASSIFY_INTENT_PROMPT,
ANALYTICS_RESPONSE_PROMPT,
} from '../constants/prompts'
import { VideoSearchParamsSchema } from '@shared/schemas/search'
import { YearInReviewData, YearInReviewDataSchema } from '@shared/schemas/yearInReview'
import type { VideoWithScenes } from '@shared/types/video'
import type { YearStats } from '@shared/types/stats'
import { ModelResponse } from '@ai/types/ai'
import { logger } from '@shared/services/logger'
import { VideoSearchParams } from '@shared/types/search'
import { MINIMAX_API_KEY, MINIMAX_MODEL } from '@ai/constants'
import { VideoAnalytics } from '@shared/types/analytics'
import { formatHistory } from '@ai/utils'

const CONTEXT_WINDOW_LIMIT = 1_000_000

let client: OpenAI | null = null

function getClient(): OpenAI {
if (!client) {
if (!MINIMAX_API_KEY) {
throw new Error('MINIMAX_API_KEY is not set')
}
client = new OpenAI({
apiKey: MINIMAX_API_KEY,
baseURL: 'https://api.minimax.io/v1',
})
}
return client
}

function stripThinkTags(text: string): string {
return text.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
}

async function generate(prompt: string): Promise<ModelResponse<string>> {
const openai = getClient()

const response = await openai.chat.completions.create({
model: MINIMAX_MODEL,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
})

const content = response.choices[0]?.message?.content ?? ''
const tokens = response.usage?.prompt_tokens ?? 0

return {
data: stripThinkTags(content).trim(),
tokens,
}
}

async function generateJSON(prompt: string): Promise<ModelResponse<string>> {
const openai = getClient()

const response = await openai.chat.completions.create({
model: MINIMAX_MODEL,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
response_format: { type: 'json_object' },
})

const content = response.choices[0]?.message?.content ?? ''
const tokens = response.usage?.prompt_tokens ?? 0

return {
data: stripThinkTags(content)
.replace(/```json|```/g, '')
.trim(),
tokens,
}
}

export const MiniMaxModel = {
async generateActionFromPrompt(
query: string,
chatHistory?: ChatMessage[],
projectInstructions?: string
): Promise<ModelResponse<VideoSearchParams>> {
const fallback = VideoSearchParamsSchema.parse({})

if (!query || query.trim() === '') return { data: fallback, tokens: 0, error: undefined }

try {
const history = formatHistory(chatHistory)
const prompt = SEARCH_PROMPT(query, history, projectInstructions)

const { data: raw, tokens } = await generateJSON(prompt)

try {
const parsed = JSON.parse(raw)
return {
data: VideoSearchParamsSchema.parse({
...parsed,
semanticQuery: null,
locations: [],
camera: null,
detectedText: null,
}),
tokens,
error: undefined,
}
} catch (parseError) {
logger.error('Failed to parse JSON: ' + parseError)
return { data: fallback, tokens: 0, error: 'Invalid JSON' }
}
} catch (err) {
logger.error('MiniMax generateActionFromPrompt failed: ' + err)
throw err
}
},

async generateAssistantMessage(
userPrompt: string,
count: number,
chatHistory?: ChatMessage[],
projectInstructions?: string
): Promise<ModelResponse<string>> {
try {
const history = chatHistory?.length
? chatHistory.map((h) => `${h.sender}: ${h.text}`).join('\n')
: ''
return await generate(ASSISTANT_MESSAGE_PROMPT(userPrompt, count, history, projectInstructions))
} catch (error) {
logger.error('MiniMax generateAssistantMessage error: ' + error)
throw error
}
},

async generateYearInReviewResponse(
stats: YearStats,
videos: VideoWithScenes[],
extraDetails: string,
projectInstructions?: string
): Promise<ModelResponse<YearInReviewData | null>> {
try {
const content = YEAR_IN_REVIEW(stats, videos, extraDetails, projectInstructions)
const { data: raw, tokens } = await generateJSON(content)

try {
const parsed = JSON.parse(raw)
const validated = YearInReviewDataSchema.parse(parsed)
return { data: validated, tokens, error: undefined }
} catch (parseError) {
logger.error('Failed to parse year in review JSON: ' + parseError)
return { data: null, tokens: 0, error: 'Invalid JSON response from AI' }
}
} catch (err) {
logger.error('MiniMax year in review error: ' + err)
throw err
}
},

async generateGeneralResponse(
userPrompt: string,
chatHistory?: ChatMessage[],
projectInstructions?: string
): Promise<ModelResponse<string>> {
try {
const history = formatHistory(chatHistory)
return await generate(GENERAL_RESPONSE_PROMPT(userPrompt, history, projectInstructions))
} catch (err) {
logger.error('MiniMax general response error: ' + err)
throw err
}
},

async classifyIntent(
prompt: string,
chatHistory?: ChatMessage[],
projectInstructions?: string
): Promise<
ModelResponse<{ type?: 'general' | 'compilation' | 'analytics' | undefined; needsVideoData?: boolean | undefined }>
> {
try {
const history = formatHistory(chatHistory)
const content = CLASSIFY_INTENT_PROMPT(prompt, history, projectInstructions)
const { data: raw, tokens } = await generateJSON(content)

return {
data: JSON.parse(raw),
tokens,
error: undefined,
}
} catch (error) {
logger.error('MiniMax classifyIntent error: ' + error)
throw error
}
},

async generateCompilationResponse(
userPrompt: string,
count: number,
chatHistory?: ChatMessage[],
projectInstructions?: string
): Promise<ModelResponse<string>> {
try {
const history = formatHistory(chatHistory)
return await generate(VIDEO_COMPILATION_MESSAGE_PROMPT(userPrompt, count, history, projectInstructions))
} catch (error) {
logger.error('MiniMax compilation response error: ' + error)
throw error
}
},

async generateAnalyticsResponse(
userPrompt: string,
analytics: VideoAnalytics,
chatHistory?: ChatMessage[],
projectInstructions?: string
): Promise<ModelResponse<string>> {
try {
const history = formatHistory(chatHistory)
return await generate(ANALYTICS_RESPONSE_PROMPT(userPrompt, analytics, history, projectInstructions))
} catch (error) {
logger.error('MiniMax analytics response error: ' + error)
throw error
}
},
}
11 changes: 9 additions & 2 deletions packages/ai/src/services/modelRouter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { GeminiModel } from '@ai/services/gemini'
import { OllamaModel } from '@ai/services/ollama'
import { MiniMaxModel } from '@ai/services/minimax'
import { logger } from '@shared/services/logger'
import {
GEMINI_API_KEY,
GEMINI_MODEL_NAME,
MINIMAX_API_KEY,
MINIMAX_MODEL,
OLLAMA_MODEL,
USE_GEMINI,
USE_MINIMAX,
USE_OLLAMA_MODEL,
} from '@ai/constants'
import { AIModel } from '@ai/types/ai'
Expand All @@ -20,11 +24,14 @@ const setupModel = () => {
if (USE_OLLAMA_MODEL && OLLAMA_MODEL) {
logger.debug(`Using Ollama Model: ${OLLAMA_MODEL}`)
activeModel = OllamaModel
} if (GEMINI_API_KEY && USE_GEMINI) {
} else if (MINIMAX_API_KEY && USE_MINIMAX) {
logger.debug(`Using MiniMax Model: ${MINIMAX_MODEL}`)
activeModel = MiniMaxModel
} else if (GEMINI_API_KEY && USE_GEMINI) {
logger.debug(`Using Gemini Model: ${GEMINI_MODEL_NAME}`)
activeModel = GeminiModel
} else {
throw new Error('No valid AI backend found. Set USE_OLLAMA_MODEL + OLLAMA_MODEL or GEMINI_API_KEY + USE_GEMINI.')
throw new Error('No valid AI backend found. Set USE_OLLAMA_MODEL + OLLAMA_MODEL, MINIMAX_API_KEY + USE_MINIMAX, or GEMINI_API_KEY + USE_GEMINI.')
}
}

Expand Down
Loading
Loading