| 1 | import { createSdkMcpServer, McpSdkServerConfigWithInstance } from '@anthropic-ai/claude-agent-sdk' |
| 2 | import { Injectable, Logger } from '@nestjs/common' |
| 3 | import { UserType } from '@yikart/common' |
| 4 | import { z } from 'zod' |
| 5 | import { AiAvailabilityService } from '../../../ai-availability' |
| 6 | import { DramaRecapService } from '../../../ai/aideo' |
| 7 | import { DramaRecapTaskStatus } from '../../../ai/libs/volcengine' |
| 8 | import { McpServerName } from '../../agent.constants' |
| 9 | import { errorResult, successResult, wrapTool } from '../mcp.utils' |
| 10 | |
| 11 | const submitDramaRecapTaskSchema = z.object({ |
| 12 | vids: z.array(z.string()), |
| 13 | dramaScriptTaskId: z.string().optional(), |
| 14 | recapText: z.string().optional(), |
| 15 | speakerConfig: z.object({ |
| 16 | appId: z.string(), |
| 17 | cluster: z.string(), |
| 18 | voiceType: z.string(), |
| 19 | }).optional(), |
| 20 | isEraseSubtitle: z.boolean().optional(), |
| 21 | fontConfig: z.object({ |
| 22 | color: z.string().optional(), |
| 23 | size: z.number().optional(), |
| 24 | name: z.string().optional(), |
| 25 | }).optional(), |
| 26 | recapStyle: z.string().optional(), |
| 27 | recapTextSpeed: z.number().min(0.5).max(2.0).optional(), |
| 28 | recapTextLength: z.number().max(5000).optional(), |
| 29 | pauseTime: z.number().min(1).max(1000).optional(), |
| 30 | allowRepeatMatch: z.boolean().optional(), |
| 31 | batchGenerateCount: z.number().min(1).optional(), |
| 32 | }) |
| 33 | |
| 34 | const getDramaRecapTaskStatusSchema = z.object({ |
| 35 | taskId: z.string(), |
| 36 | }) |
| 37 | |
| 38 | export enum DramaRecapToolName { |
| 39 | SubmitDramaRecapTask = 'submitDramaRecapTask', |
| 40 | GetDramaRecapTaskStatus = 'getDramaRecapTaskStatus', |
| 41 | } |
| 42 | |
| 43 | @Injectable() |
| 44 | export class DramaRecapMcp { |
| 45 | private readonly logger = new Logger(DramaRecapMcp.name) |
| 46 | |
| 47 | constructor( |
| 48 | private readonly dramaRecapService: DramaRecapService, |
| 49 | private readonly aiAvailability: AiAvailabilityService, |
| 50 | ) { } |
| 51 | |
| 52 | createSubmitDramaRecapTaskTool(userId: string, userType: UserType) { |
| 53 | return wrapTool( |
| 54 | this.logger, |
| 55 | DramaRecapToolName.SubmitDramaRecapTask, |
| 56 | `Submit a drama recap (narration) task to generate narrated videos from short drama clips. |
| 57 | |
| 58 | **IMPORTANT: DO NOT call getVideoInfo before this task.** |
| 59 | |
| 60 | **Parameters**: |
| 61 | - vids: Array of video VIDs (vid://xxx format). URLs auto-uploaded. |
| 62 | - dramaScriptTaskId: Optional (auto-generated if not provided) |
| 63 | - recapText: Custom narration text (optional, auto-generated if not provided) |
| 64 | - speakerConfig: Voice synthesis config (appId, cluster, voiceType) |
| 65 | - isEraseSubtitle: Erase subtitles (default: true) |
| 66 | - fontConfig: Subtitle font config (color, size, name) |
| 67 | - recapStyle: AI narration style (e.g., "humorous", "suspenseful", "light") |
| 68 | - recapTextSpeed: Speech speed (0.5-2.0, default: 1.2) |
| 69 | - recapTextLength: Expected text length in chars (max: 5000) |
| 70 | - pauseTime: Pause between sentences in ms (1-1000, default: 120) |
| 71 | - batchGenerateCount: Number of videos to generate (default: 1) |
| 72 | |
| 73 | Processing time: ~10 minutes per 1-minute video. Returns taskId and dramaScriptTaskId.`, |
| 74 | submitDramaRecapTaskSchema.shape, |
| 75 | async ({ ...params }) => { |
| 76 | const result = await this.dramaRecapService.submitDramaRecapTask({ |
| 77 | userId, |
| 78 | userType, |
| 79 | ...params, |
| 80 | }) |
| 81 | return successResult(`Drama recap task submitted successfully. TaskId: ${result.taskId}, DramaScriptTaskId: ${result.dramaScriptTaskId}`) |
| 82 | }, |
| 83 | this.aiAvailability, |
| 84 | ) |
| 85 | } |
| 86 | |
| 87 | createGetDramaRecapTaskStatusTool(userId: string, userType: UserType) { |
| 88 | return wrapTool( |
| 89 | this.logger, |
| 90 | DramaRecapToolName.GetDramaRecapTaskStatus, |
| 91 | `Get drama recap task status and results. |
| 92 | |
| 93 | **Returns**: Status (Processing/Completed/Failed), outputVid and outputUrl on completion. |
| 94 | |
| 95 | **Error Codes**: 400100 (Invalid params), 500100 (Internal error), 500110 (Slicing failed), 500111 (Audio extraction failed), 500120 (Script restoration failed), 500130 (Narration generation failed), 400200 (Duration too long, max 60min), 400201 (Too many words).`, |
| 96 | getDramaRecapTaskStatusSchema.shape, |
| 97 | async ({ taskId }) => { |
| 98 | const result = await this.dramaRecapService.getDramaRecapTask({ |
| 99 | userId, |
| 100 | userType, |
| 101 | taskId, |
| 102 | }) |
| 103 | |
| 104 | if (result.status === DramaRecapTaskStatus.Completed) { |
| 105 | if (result.outputVid) { |
| 106 | return successResult(`Task completed successfully! Output video VID: ${result.outputVid}${result.outputUrl ? `, URL: ${result.outputUrl}` : ''}`) |
| 107 | } |
| 108 | else { |
| 109 | return successResult('Task completed successfully!') |
| 110 | } |
| 111 | } |
| 112 | else if (result.status === DramaRecapTaskStatus.Processing) { |
| 113 | return successResult('Task is still running. Please continue to wait...') |
| 114 | } |
| 115 | else { |
| 116 | return errorResult(`Task failed: ${result.errorMessage || 'Unknown error'}`) |
| 117 | } |
| 118 | }, |
| 119 | this.aiAvailability, |
| 120 | ) |
| 121 | } |
| 122 | |
| 123 | createServer(userId: string, userType: UserType): McpSdkServerConfigWithInstance { |
| 124 | return createSdkMcpServer({ |
| 125 | name: McpServerName.DramaRecap, |
| 126 | version: '1.0.0', |
| 127 | tools: [ |
| 128 | this.createSubmitDramaRecapTaskTool(userId, userType), |
| 129 | this.createGetDramaRecapTaskStatusTool(userId, userType), |
| 130 | ], |
| 131 | }) |
| 132 | } |
| 133 | } |
| 134 |