aimakeall-mcp 0.14.3 → 0.14.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -0
- package/lib/cloud-tools.mjs +142 -57
- package/lib/config.mjs +1 -1
- package/lib/durable-channel-client.mjs +30 -0
- package/lib/durable-channel-contract.mjs +70 -0
- package/lib/durable-channel-journal.mjs +58 -0
- package/lib/durable-channel-request.mjs +375 -0
- package/lib/durable-remake-contract.mjs +88 -0
- package/lib/durable-remake-request.mjs +375 -0
- package/lib/durable-speech.mjs +18 -4
- package/lib/durable-voice-contract.mjs +62 -0
- package/lib/narration-timeline.mjs +189 -0
- package/lib/review-plan-client.mjs +73 -0
- package/lib/review-production-contract.mjs +198 -0
- package/lib/review-tools.mjs +40 -11
- package/lib/speech-production-journal.mjs +109 -0
- package/lib/speech-production-profile.mjs +21 -0
- package/lib/speech-production.mjs +161 -0
- package/lib/studio-plan-client.mjs +64 -0
- package/lib/studio-plan-contract.mjs +128 -0
- package/lib/studio-plan-request.mjs +372 -0
- package/lib/tts-alignment.mjs +69 -0
- package/lib/voice-catalog.mjs +33 -0
- package/lib/voice-recommendation-client.mjs +40 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,6 +5,12 @@ Claude Code·Codex 같은 MCP 클라이언트에서 자연어로 AImakeAll 영
|
|
|
5
5
|
- **클라우드 툴**: 기획 → 씬 이미지/영상 생성 → TTS/음악 → 타임라인 스티치 → YouTube 퍼블리시 (PAT 인증, aimakeall.com API)
|
|
6
6
|
- **로컬 툴**: mp4 렌더 — **항상 이 PC의 AImakeAll 컴패니언(127.0.0.1:9876)에서 수행**됩니다. 서버는 렌더하지 않습니다.
|
|
7
7
|
|
|
8
|
+
## 0.14.4 공유 실행 호환성 업데이트
|
|
9
|
+
|
|
10
|
+
채널 실측 작업의 계정별 복구와 실제 발화 자막·음성 제작 스타일 보존을 추가했습니다.
|
|
11
|
+
공유 실행은 서버가 지원을 명시한 경우에만 사용하며 기존 단일 서버와 호환됩니다.
|
|
12
|
+
운영 PostgreSQL 전환이나 자동 확장이 이 패키지 게시만으로 활성화되지는 않습니다.
|
|
13
|
+
|
|
8
14
|
## 0.14.3 리뷰 분석 업데이트
|
|
9
15
|
|
|
10
16
|
전편 분석의 서버 실행 방식 조회, 계정·서버별 체크포인트 고정, 알려진 작업의 읽기 전용
|
|
@@ -124,8 +130,132 @@ plan_shorts_video → (씬마다) generate_scene_image → get_generation_job(co
|
|
|
124
130
|
- 성공 응답의 `provider`, `primaryProvider`, `providerFallback`, `accountCostEvents`로 실제 합성 공급자와 비용을 확인합니다. 폴백 후 새 `productionPath`에 실제 보이스를 저장하며 이전 제작 핸들은 바꾸지 않습니다. 항상 최신 핸들을 다음 단계에 넘기세요.
|
|
125
131
|
- 제출 시간 초과·네트워크 단절·접수 후 결과 불명, 인증·입력·안전정책 오류에는 다른 유료 생성을 자동 실행하지 않습니다. 오류의 `fallbackStatus`, `fallbackReason`, `fallbackAvailable`, 접수된 비용을 확인하고 기존 작업부터 조회하세요. 실패했다고 비용이 없거나 환불되었다는 뜻은 아닙니다.
|
|
126
132
|
- ElevenLabs의 정확한 문자별 타임스탬프를 선택한 요청은 추정 자막으로 대체하지 않습니다. 다른 공급자 일반 합성이 ElevenLabs로 전환되더라도 `timingSource:"estimated-from-audio-duration"`인 자막이 정밀 타임스탬프로 바뀌지는 않습니다. 사용자 PC의 Companion 직접 Edge TTS는 서버 폴백 체인 밖입니다.
|
|
133
|
+
|
|
134
|
+
공유 실행용 `start_speech_generation`은 공급자 한 곳과 `requestId`를 고정합니다.
|
|
135
|
+
ElevenLabs의 실제 자막은 `options.includeAlignment:true`로 오디오와 같은 작업에서
|
|
136
|
+
요청하고, 원래 제작 핸들의 `productionProfile`을 `options.productionProfile`로 전달합니다.
|
|
137
|
+
`get_generation_job` 완료 응답은 보호된 오디오 링크와 검증된 `subtitleLines`,
|
|
138
|
+
`timingSource:"provider-character-alignment"`를 반환합니다. 응답이 끊겨도 같은 ID를
|
|
139
|
+
조회하며 다른 공급자나 기존 동기 TTS로 다시 합성하지 않습니다. 이 비동기 도구가
|
|
140
|
+
기존 제작 핸들을 갱신해야 한다면 아래 제작용 도구 흐름을 사용합니다.
|
|
141
|
+
|
|
142
|
+
### 기존 음성 도구의 공유 실행 연결 (다음 릴리스용 변경, 아직 게시 전)
|
|
143
|
+
|
|
144
|
+
- `tts_narration`과 `tts_narration_with_captions`는 인증된
|
|
145
|
+
`GET /api/tracker/tts/mcp-execution-capability`로 실행 방식을 먼저 확인합니다.
|
|
146
|
+
서버가 명시적으로 `binary`를 반환할 때만 기존 동기 합성을 사용합니다.
|
|
147
|
+
구형 서버의 404, 통신 실패, 알 수 없는 계약을 동기 합성 허용으로 해석하지 않습니다.
|
|
148
|
+
따라서 **대응 서버를 먼저 배포한 뒤 MCP를 게시·업데이트**해야 합니다.
|
|
149
|
+
- 공유 유료 합성은 제작 핸들의 `productionId`로 안정된 작업 ID를 만들며,
|
|
150
|
+
핸들이 없으면 `requestId`(영문·숫자·`_`·`-`, 16~80자)가 필요합니다.
|
|
151
|
+
Typecast → ElevenLabs → Supertone → Edge → Supertonic2의 선택 우선순위와
|
|
152
|
+
저장된 캐스팅을 유지합니다. 접수 뒤 공급자 변경·자동 유료 재합성은 없습니다.
|
|
153
|
+
- 접수 결과의 `generationJobId`로 `finish_speech_generation`을 호출합니다.
|
|
154
|
+
완료 전이면 상태만 반환하고, 완료되면 보호된 오디오를 PC에 저장하고 **새로운**
|
|
155
|
+
`productionPath`를 반환합니다. 이를 `stitch_timeline`에 전달하세요.
|
|
156
|
+
원래 제작 핸들의 제목·강조어·디자인·캐스팅은 보존하며 원본 파일을 덮어쓰지 않습니다.
|
|
157
|
+
ElevenLabs 자막은 공급자 문자 정렬, 나머지는 실제 오디오 길이에 따른 추정임을 표시합니다.
|
|
158
|
+
- 제출 전에 비공개 로컬 기록을 저장합니다. 응답 유실·MCP 재연결 후에는 같은 기록의
|
|
159
|
+
작업을 조회·다운로드할 뿐 다시 POST하지 않습니다. 입력이 달라지면 자동으로 새 ID를
|
|
160
|
+
만들지 않습니다. 결과 불명을 해결하려고 기록 삭제·PAT 교체·새 ID 합성을 하지 마세요.
|
|
161
|
+
의도적으로 새 음성이 필요한 경우 기존 비용/상태를 확인한 뒤 별도 `requestId`를 지정합니다.
|
|
162
|
+
- 기록은 `stateDir/speech-production-v1`에 저장되며 PAT 원문은 저장하지 않습니다.
|
|
163
|
+
다른 PAT/서버의 기록, 변조·불완전 파일은 거절합니다. 음성 파일의 TTL 정리와 달리
|
|
164
|
+
제출 기록은 자동 삭제하지 않습니다(최대 4,096건/합계 64MiB, 개별 2MiB).
|
|
165
|
+
한도에 도달하면 새 유료 접수를 차단하며,
|
|
166
|
+
기존 ID의 조회·다운로드는 계속 가능합니다.
|
|
167
|
+
- 이 완료 도구는 위 두 제작 도구가 남긴 기록을 사용합니다. 독립 도구
|
|
168
|
+
`start_speech_generation`의 작업은 계속 `get_generation_job`으로 조회합니다.
|
|
169
|
+
리메이크/리뷰 행별 합성은 아래의 별도 작업 흐름을 사용합니다.
|
|
170
|
+
|
|
171
|
+
### 리메이크·리뷰 행별 공유 음성 (다음 릴리스용 변경, 아직 게시 전)
|
|
172
|
+
|
|
173
|
+
- `prepare_remake_timeline`, `prepare_review_timeline`은 인증된
|
|
174
|
+
`/api/tracker/mcp/narration/{remake|review}/execution-capability`를 먼저 확인합니다.
|
|
175
|
+
`binary`일 때만 기존 단일 서버 실행을 사용합니다. 구형 서버/통신 오류로 폴백하지
|
|
176
|
+
않으므로 대응 서버를 먼저 배포해야 합니다.
|
|
177
|
+
- 공유 모드에서는 전체 행의 합성 비용에 동의한 뒤 시작하세요. 리메이크는 고정
|
|
178
|
+
`requestId`가 필요하고 리뷰는 제작 핸들의 `productionId`에서 작업 ID를 결정합니다.
|
|
179
|
+
Typecast·ElevenLabs·Supertone의 명시 보이스가 필요합니다. 공유 Edge/Supertonic2
|
|
180
|
+
행별 실행은 아직 지원하지 않으며 유료·동기 대체 실행을 하지 않습니다.
|
|
181
|
+
- 반환된 `operationId`를 `continue_narrated_timeline`에 전달합니다. 이 도구는
|
|
182
|
+
**조회 전용이 아닙니다.** 기존 행을 확인한 후 아직 제출하지 않은 행을 호출당 최대
|
|
183
|
+
1건 합성합니다. 접수한 행은 같은 작업 ID로 조회만 하며 실패·응답 유실 시
|
|
184
|
+
다시 합성하지 않습니다. 완료 행의 비용 영수증도 함께 반환합니다.
|
|
185
|
+
- 리뷰는 각 음성의 실제 길이가 원본 구간을 넘으면 다음 행을 합성하기 전에 멈춥니다.
|
|
186
|
+
최종 서버 조립에서도 소유권·입력·비용 확정·오디오 무결성·길이를 다시 확인합니다.
|
|
187
|
+
원래 제목·자막 디자인·자동 소재 편집·캐스팅을 유지하고, 로컬 원본 경로는 서버에
|
|
188
|
+
보내지 않습니다. 모두 완료되면 `payloadPath`를 `render_start`에 전달합니다.
|
|
189
|
+
- `stateDir/narration-timeline-v1`에 서버/PAT에 결속한 기록을 저장합니다.
|
|
190
|
+
기록 삭제·PAT 교체·새 ID로 결과 불명을 우회하지 마세요. 기존 음성 조립은 새
|
|
191
|
+
TTS 비용을 발생시키지 않지만 서버의 렌더 이용 한도는 적용됩니다. 최종 미디어가
|
|
192
|
+
TTL로 정리되면 자동 재합성/재조립하지 않습니다. 보관 기록은 자동 삭제하지 않으며
|
|
193
|
+
4,096개/64MiB 한도에 이르면 새 접수를 중단합니다.
|
|
194
|
+
|
|
127
195
|
- 이미지·영상·LLM 폴백은 대응 서버가 지원하는 동일 기능/입력 경로에서만 동작합니다. SUNO, 음성 클로닝, 정밀 정렬 등 모든 독점 기능에 호환 대체가 있는 것은 아닙니다. 상태 조회만으로 미확인 유료 작업을 재생성하지 마세요.
|
|
128
196
|
|
|
197
|
+
### 보이스 추천 공유 실행 (다음 릴리스용 변경, 아직 게시 전)
|
|
198
|
+
|
|
199
|
+
- `recommend_voice`는 대응 서버의 capability를 확인합니다. `legacy-sync`일 때만
|
|
200
|
+
기존 단일 서버 경로를 쓰며, 공유 모드에서는 샘플 관측/추천을 하나의 영속 작업으로
|
|
201
|
+
실행합니다. 대응 서버를 먼저 배포해야 하며 404·통신 실패를 동기 폴백으로 취급하지 않습니다.
|
|
202
|
+
- 동일한 주제·대본·프로필·샘플·요청 공급자 조합은 재연결 후 기존 작업만 조회합니다.
|
|
203
|
+
새 유료 추천을 명시적으로 원할 때만 새 `requestId`(UUID)를 지정하세요. 불명 작업을
|
|
204
|
+
우회하려고 ID·PAT·기록을 변경하지 마세요. 응답의 `backgroundJobId`와 비용 영수증을 보존합니다.
|
|
205
|
+
- Typecast → ElevenLabs → Supertone → Edge → Supertonic2 순서는 TTS 공급자 선택입니다.
|
|
206
|
+
공유 **추천 LLM**은 Gemini 3 Flash입니다. 계정의 사용 가능한 키를 기준으로 호출 전에
|
|
207
|
+
EvoLink를 우선 선택하고, 해당 키가 없으면 KIE를 선택합니다. KIE-only 계정의 샘플
|
|
208
|
+
스트리밍 분석도 지원합니다. 두 키 모두 없으면 무료 TTS 공급자의 샘플 없는 카탈로그
|
|
209
|
+
추천만 가능합니다. 호출 후 공급자를 바꾸거나 샘플 분석을 조용히 생략하지 않습니다.
|
|
210
|
+
- 샘플은 앞 90초 이내만 관측하고 단계별 비용 기록 확인 후 다음 요청을 보냅니다.
|
|
211
|
+
요청 실패/비용 확인 불명/JSON 오류에는 자동 유료 재시도를 하지 않습니다.
|
|
212
|
+
보이스·자막 스타일·제작 프로필은 최신 `productionPath`로 다음 합성에 이어집니다.
|
|
213
|
+
- `stateDir/voice-recommendation-v1`은 별도 비공개 journal입니다(최대 4,096건,
|
|
214
|
+
결과 파일당 64KiB). PAT 원문·카탈로그·샘플 오디오는 기록하지 않고 자동 삭제하지 않습니다.
|
|
215
|
+
저장소/서버 작업이 정리되거나 손상되면 자동 재추천하지 않으므로 고객지원에 문의하세요.
|
|
216
|
+
|
|
217
|
+
### 리뷰 기획 공유 실행 (다음 릴리스용 변경, 아직 게시 전)
|
|
218
|
+
|
|
219
|
+
- `plan_review_video`는 `/api/tracker/review/plan/execution-capability`의
|
|
220
|
+
명시적 `queue-v2`에 따라 영속 작업을 제출합니다. `legacy-sync`일 때만
|
|
221
|
+
기존 동기 기획을 사용합니다. 404·인증 실패·통신 오류로 유료 동기 기획을
|
|
222
|
+
대신 실행하지 않으므로 **대응 서버를 먼저 배포한 뒤 MCP를 업데이트**합니다.
|
|
223
|
+
- 같은 `reviewSourcePath`·리뷰 설정·선택 장면으로 다시 호출하면 기존 작업만
|
|
224
|
+
조회합니다. 응답의 `backgroundJobId`와 비용 영수증을 보존하세요.
|
|
225
|
+
제출 결과가 불명확하거나 서버 기록이 만료되면 자동 재제출하지 않습니다.
|
|
226
|
+
기록 삭제·PAT 교체·새 핸들로 불명 작업을 우회하지 말고 고객지원에 문의하세요.
|
|
227
|
+
- 전체 분석/원본 재검증 완료 조건, 전체 전사와 원본 구간 근거, 말투·자막의
|
|
228
|
+
사실/해석 구분을 유지합니다. 원본 핸들이 변경되거나 없어지면 이미 받은 결과와
|
|
229
|
+
섞지 않고 기존 작업 ID·확정 비용을 포함한 오류를 반환합니다. 선택한 TTS 공급자와
|
|
230
|
+
보이스는 제작 핸들에 보존하며 보이스만 변경해도 기획 LLM을 다시 호출하지 않습니다.
|
|
231
|
+
- `stateDir/review-plan-v1`에는 비공개 제출·결과 기록을 저장합니다.
|
|
232
|
+
결과에는 원본 근거/전사가 포함되므로 민감한 자료처럼 보관하세요. PAT 원문과
|
|
233
|
+
로컬 원본 파일 경로는 저장하지 않습니다. 최대 32건/결과 파일당 20MiB이며
|
|
234
|
+
자동 삭제하지 않습니다. 한도에 이르면 새 제출을 차단하고 기존 작업 조회는 유지합니다.
|
|
235
|
+
|
|
236
|
+
### 쇼츠·커머스·뮤직비디오 기획 공유 실행 (다음 릴리스용 변경, 아직 게시 전)
|
|
237
|
+
|
|
238
|
+
- `plan_shorts_video`, `plan_commerce_video`, `plan_music_video`는 각각의 인증된
|
|
239
|
+
`/plan/execution-capability`에서 `queue-v2`를 확인하면 전용 `/plan/jobs`에
|
|
240
|
+
제출합니다. `legacy-sync`만 기존 동기 실행을 허용하며, 404·인증·통신 오류는
|
|
241
|
+
동기 폴백 허가가 아닙니다. 대응 서버 배포 후 MCP 새 버전을 게시해야 합니다.
|
|
242
|
+
- 동일한 기획 옵션으로 다시 호출하면 알려진 작업 ID만 조회합니다. 접수 응답이
|
|
243
|
+
유실돼 ID가 불명확해도 자동 재제출하지 않습니다. 기록·PAT·입력을 바꾸어
|
|
244
|
+
불명 작업을 우회하지 마세요. 반환된 `backgroundJobId`와 비용 영수증을 보존합니다.
|
|
245
|
+
- 커머스 기획은 파일을 열기 전에 기존 접수를 조회하고, 동일 경로의 상품/모델 사진
|
|
246
|
+
바이트까지 제출 당시 입력과 대조합니다. 사진 변경·삭제 시 기존 결과를 새 입력과
|
|
247
|
+
섞지 않으며 기존 작업 ID·비용을 포함한 오류를 반환합니다.
|
|
248
|
+
- 자막·제작 프로필, 캐릭터, 이미지 소스/모션 선택, 채널 규격, 상품 조사 근거,
|
|
249
|
+
음악 구간을 유지합니다. 결과 핸들 경로는 새로 만들지만 복구된 기획의
|
|
250
|
+
`productionId`는 유지하므로 후속 영속 TTS를 새 제작으로 오인하지 않습니다.
|
|
251
|
+
리뷰 기획 복구에도 같은 제작 ID 보존을 적용합니다.
|
|
252
|
+
- `stateDir/studio-plan-v1`은 비공개 기록입니다. 최대 32개/결과 파일당 10MiB이며
|
|
253
|
+
자동 정리하지 않습니다. 입력 원문/사진/PAT/로컬 파일 경로는 기록하지 않고
|
|
254
|
+
입력 digest와 기획 결과·확정 비용을 저장합니다. 결과에 민감한 내용이 있을 수
|
|
255
|
+
있으므로 보호하세요. 한도에 이르면 새 제출만 차단하고 기존 작업 조회는 유지합니다.
|
|
256
|
+
- 이것은 시나리오 기획 연결이며 상품 웹 조사, 음악 생성/가사 정렬, 원본 영상 분석,
|
|
257
|
+
교사 비동기 과금 등 다른 경로까지 모두 공유 실행된다는 뜻은 아닙니다.
|
|
258
|
+
|
|
129
259
|
## 장면 이미지 준비와 영상 표현 선택
|
|
130
260
|
|
|
131
261
|
다음 설정은 0.12.0부터 지원합니다. 구형 클라이언트는 위의 자동 업데이트 전환 안내에 따라 한 번 전환하고 유휴 상태에서 다시 연결해야 새 입력과 도구 안내를 사용할 수 있습니다. 이미지 모션 렌더에는 해당 기능을 지원하는 최신 컴패니언 런타임도 필요합니다.
|
package/lib/cloud-tools.mjs
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createChannelMeasurementClient } from "./durable-channel-client.mjs";
|
|
2
|
+
import { createVoiceRecommendationClient } from "./voice-recommendation-client.mjs";
|
|
3
|
+
import { createStudioPlanningClient, studioPlanReceipt, withStudioPlanReceipt } from './studio-plan-client.mjs';
|
|
4
|
+
import { compactVoiceCatalog } from "./voice-catalog.mjs";
|
|
1
5
|
|
|
2
6
|
import { createReadStream, readFileSync, statSync } from "node:fs";
|
|
3
7
|
import path from "node:path";
|
|
@@ -25,6 +29,8 @@ import { canonicalYoutubeWatchUrl } from "./payload-guard.mjs";
|
|
|
25
29
|
import { createUsageEventId } from "./usage-event.mjs";
|
|
26
30
|
import { fetchGenerationJob, GENERATION_JOB_PATH, submitGenerationJob, requestGenerationJobAction } from "./generation-jobs.mjs";
|
|
27
31
|
import { durableSpeechToolSchema, submitDurableSpeech, durableSpeechResult } from "./durable-speech.mjs";
|
|
32
|
+
import { trySharedSpeechProduction, finishSpeechProduction } from "./speech-production.mjs";
|
|
33
|
+
import { trySharedNarrationTimeline, continueNarratedTimeline } from "./narration-timeline.mjs";
|
|
28
34
|
import { loudnessDbfsFromWav, silenceRatioFromWav } from "./wav-dsp.mjs";
|
|
29
35
|
import { trimWavToSeconds } from "./wav-trim.mjs";
|
|
30
36
|
import { registerComposerTools } from "./composer-tools.mjs";
|
|
@@ -100,6 +106,10 @@ function jsonResult(value) {
|
|
|
100
106
|
return textResult(JSON.stringify(value, null, 2));
|
|
101
107
|
}
|
|
102
108
|
|
|
109
|
+
function speechProductionResult(value) {
|
|
110
|
+
return textResult(JSON.stringify(value, null, 2), { isError: !["pending", "running", "completed"].includes(value.status) });
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
// JSON + 이미지 블록 결과 — 비전 지원 클라이언트의 에이전트가 산출물을 "직접 보고"
|
|
104
114
|
// QC(1층 검증)할 수 있게 한다. 비전 미지원 클라이언트는 이미지 블록을 무시한다.
|
|
105
115
|
function jsonWithImagesResult(value, images = []) {
|
|
@@ -159,6 +169,10 @@ export function wrapCloudHandler(config, handler) {
|
|
|
159
169
|
try {
|
|
160
170
|
return await handler(args, extra);
|
|
161
171
|
} catch (error) {
|
|
172
|
+
if (error?.paidReplayAllowed === false) return textResult(JSON.stringify({
|
|
173
|
+
error: describeLocalError(error), code: error.code, ...studioPlanReceipt(error),
|
|
174
|
+
guidance: "기존 작업 ID와 비용을 확인하세요. 결과 불명 작업을 자동으로 재제출하지 마세요.",
|
|
175
|
+
}), { isError: true });
|
|
162
176
|
if (error instanceof AimakeallApiError) {
|
|
163
177
|
const retry = error.retryAfterSeconds ? ` (${error.retryAfterSeconds}초 후 재시도)` : "";
|
|
164
178
|
if (error.partialProviderUsage || error.accountCostEvents.length || error.fallbackStatus) {
|
|
@@ -176,6 +190,32 @@ export function wrapCloudHandler(config, handler) {
|
|
|
176
190
|
};
|
|
177
191
|
}
|
|
178
192
|
|
|
193
|
+
async function runStudioPlan(config, api, operation, intent, buildBody, apply) {
|
|
194
|
+
const client = createStudioPlanningClient(config, api, operation, intent), recovered = await client.recover();
|
|
195
|
+
return withStudioPlanReceipt(recovered, async () => {
|
|
196
|
+
const body = await buildBody();
|
|
197
|
+
let payload = recovered;
|
|
198
|
+
if (payload) await client.assertInputMatches(body);
|
|
199
|
+
else {
|
|
200
|
+
const mode = await client.readCapability();
|
|
201
|
+
if (mode === 'legacy-sync') payload = await client.legacy(body);
|
|
202
|
+
else if (mode === 'queue-v2') payload = await client.run(body);
|
|
203
|
+
else throw Object.assign(Error('공유 시나리오 실행을 현재 사용할 수 없습니다.'), { code: 'STUDIO_PLAN_UNAVAILABLE', paidReplayAllowed: false });
|
|
204
|
+
}
|
|
205
|
+
return withStudioPlanReceipt(payload, () => apply(payload));
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function studioPlanningContext(payload) {
|
|
210
|
+
return {
|
|
211
|
+
// Immutable local handles may have new paths, but a recovered plan is the
|
|
212
|
+
// same production. Downstream durable TTS must not get a fresh random ID.
|
|
213
|
+
...(payload?.backgroundJobId ? { productionId: 'studio-plan:' + payload.backgroundJobId } : {}),
|
|
214
|
+
planningReceipt: studioPlanReceipt(payload),
|
|
215
|
+
charactersUsed: payload?.charactersUsed || [],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
179
219
|
// 씬 응답에서 모델 컨텍스트에 유용한 필드만 추린다 (base64/내부 필드 제외).
|
|
180
220
|
function trimPlanScenes(scenes) {
|
|
181
221
|
return (Array.isArray(scenes) ? scenes : []).map((scene) => ({
|
|
@@ -262,7 +302,7 @@ export function registerCloudTools(server, config, api) {
|
|
|
262
302
|
|
|
263
303
|
server.tool(
|
|
264
304
|
"start_speech_generation",
|
|
265
|
-
"지정한 ElevenLabs/Typecast/Supertone 한 곳에 비동기 내레이션 작업을 접수합니다. 공유 실행 저장소가 필요합니다. requestId를 유지하고 get_generation_job으로 조회하세요. 대체 공급자·자동 재합성·기존 바이너리 TTS로 전환하지 않으며, 완료 오디오는 서버 소유 링크로 반환됩니다.",
|
|
305
|
+
"지정한 ElevenLabs/Typecast/Supertone 한 곳에 비동기 내레이션 작업을 접수합니다. 공유 실행 저장소가 필요합니다. ElevenLabs는 options.includeAlignment=true로 실제 발화 자막도 함께 생성하며 productionProfile로 원래 말투·스타일을 유지합니다. requestId를 유지하고 get_generation_job으로 조회하세요. 대체 공급자·자동 재합성·기존 바이너리 TTS로 전환하지 않으며, 완료 오디오는 서버 소유 링크로 반환됩니다.",
|
|
266
306
|
durableSpeechToolSchema,
|
|
267
307
|
wrapCloudHandler(config, async input => {
|
|
268
308
|
const result = await submitDurableSpeech(api, input);
|
|
@@ -270,6 +310,13 @@ export function registerCloudTools(server, config, api) {
|
|
|
270
310
|
}),
|
|
271
311
|
);
|
|
272
312
|
|
|
313
|
+
server.tool(
|
|
314
|
+
"finish_speech_generation",
|
|
315
|
+
"tts_narration/tts_narration_with_captions가 공유 모드에서 반환한 기존 작업만 조회해 완료 오디오와 자막을 PC에 저장하고 productionPath를 반환합니다. 합성 POST·재과금 없음. MCP 재시작 뒤에도 같은 ID를 사용하세요. start_speech_generation 단독 작업에는 해당 로컬 제작 기록이 없어 사용할 수 없습니다.",
|
|
316
|
+
{ generationJobId: z.string().regex(/^[a-zA-Z0-9_-]{16,80}$/u) },
|
|
317
|
+
wrapCloudHandler(config, async ({ generationJobId }) => speechProductionResult(await finishSpeechProduction(api, config, generationJobId))),
|
|
318
|
+
);
|
|
319
|
+
|
|
273
320
|
server.tool(
|
|
274
321
|
"resume_generation_job",
|
|
275
322
|
"내 계정의 기존 생성 작업에서 접수된 공급자 작업 상태·결과를 복구합니다. 원래 유료 생성은 다시 제출하지 않습니다. get_generation_job의 workflow.canResume이 true일 때 사용하세요.",
|
|
@@ -346,7 +393,7 @@ export function registerCloudTools(server, config, api) {
|
|
|
346
393
|
|
|
347
394
|
server.tool(
|
|
348
395
|
"plan_shorts_video",
|
|
349
|
-
"쇼츠 영상 기획(시나리오·씬별 이미지 프롬프트·내레이션)을 생성합니다. topic 또는 copy 중 하나는 필수. 결과 scenes의 imagePrompt는 generate_scene_image로, skill은 씬 영상 프롬프트의 videoStylePreset으로, emphasisKeywords는 stitch_timeline의 emphasisKeywords로 이어집니다(하단자막 단어 강조).",
|
|
396
|
+
"쇼츠 영상 기획(시나리오·씬별 이미지 프롬프트·내레이션)을 생성합니다. topic 또는 copy 중 하나는 필수. 결과 scenes의 imagePrompt는 generate_scene_image로, skill은 씬 영상 프롬프트의 videoStylePreset으로, emphasisKeywords는 stitch_timeline의 emphasisKeywords로 이어집니다(하단자막 단어 강조). 공유 모드에서는 제출 기록을 보존하고 같은 입력 재연결은 기존 작업만 조회합니다. 접수 불명 작업은 자동 재제출하지 않습니다.",
|
|
350
397
|
{
|
|
351
398
|
productionProfile: productionFields.productionProfile,
|
|
352
399
|
...visualProductionFields,
|
|
@@ -384,8 +431,9 @@ export function registerCloudTools(server, config, api) {
|
|
|
384
431
|
return textResult("topic 또는 copy 중 하나는 입력해야 합니다.", { isError: true });
|
|
385
432
|
}
|
|
386
433
|
const visualSettings = resolveVisualSettings(null, { productionProfile, imageSourceMode, visualMotionMode });
|
|
387
|
-
|
|
388
|
-
|
|
434
|
+
return runStudioPlan(config, api, 'shorts',
|
|
435
|
+
{ ...visualSettings, productionProfile, durationTargetSec, categoryId, channelFingerprint, copy, sceneCount, characters, targetCustomer, tone, topic },
|
|
436
|
+
() => ({
|
|
389
437
|
...visualSettings,
|
|
390
438
|
categoryId,
|
|
391
439
|
productionProfile,
|
|
@@ -400,17 +448,16 @@ export function registerCloudTools(server, config, api) {
|
|
|
400
448
|
targetCustomer,
|
|
401
449
|
tone,
|
|
402
450
|
topic,
|
|
403
|
-
},
|
|
404
|
-
method: "POST",
|
|
405
|
-
timeoutMs: 240_000,
|
|
406
|
-
});
|
|
451
|
+
}), payload => {
|
|
407
452
|
const productionPath = saveProductionContext(config.stateDir, {
|
|
453
|
+
...studioPlanningContext(payload),
|
|
408
454
|
topic, visualSettings, autoCompose: productionProfile?.autoComposeEnabled ?? true,
|
|
409
455
|
productionProfile: payload?.productionProfile, narrationScript: payload?.narrationScript,
|
|
410
456
|
narrationSource: payload?.narrationSource, tagline: payload?.tagline, emphasisKeywords: payload?.emphasisKeywords || [],
|
|
411
457
|
channelFingerprint, scenes: trimPlanScenes(payload?.scenes),
|
|
412
458
|
});
|
|
413
459
|
return jsonResult({
|
|
460
|
+
...studioPlanReceipt(payload),
|
|
414
461
|
productionPath,
|
|
415
462
|
...visualSettings,
|
|
416
463
|
productionProfile: payload?.productionProfile,
|
|
@@ -426,6 +473,7 @@ export function registerCloudTools(server, config, api) {
|
|
|
426
473
|
specWarnings: Array.isArray(payload?.specWarnings) && payload.specWarnings.length ? payload.specWarnings : undefined,
|
|
427
474
|
tagline: payload?.tagline,
|
|
428
475
|
});
|
|
476
|
+
});
|
|
429
477
|
}),
|
|
430
478
|
);
|
|
431
479
|
|
|
@@ -508,7 +556,7 @@ export function registerCloudTools(server, config, api) {
|
|
|
508
556
|
|
|
509
557
|
server.tool(
|
|
510
558
|
"plan_commerce_video",
|
|
511
|
-
"제품 홍보 영상 기획을 생성합니다. 제품 사진 파일 경로가 최소 1장 필요합니다 (이 PC의 로컬 경로).",
|
|
559
|
+
"제품 홍보 영상 기획을 생성합니다. 제품 사진 파일 경로가 최소 1장 필요합니다 (이 PC의 로컬 경로). 공유 모드에서는 같은 입력으로 다시 호출해 기존 작업만 조회하며 사진 변경 시 이전 결과를 적용하지 않습니다. 접수 불명 작업은 자동 재제출하지 않습니다.",
|
|
512
560
|
{
|
|
513
561
|
...visualProductionFields,
|
|
514
562
|
productionProfile: productionFields.productionProfile,
|
|
@@ -531,13 +579,16 @@ export function registerCloudTools(server, config, api) {
|
|
|
531
579
|
},
|
|
532
580
|
wrapCloudHandler(config, async ({ productName, productImagePaths, modelImagePath = "", description = "", targetCustomer = "", tone = "자동 추천", categoryId = "ecommerce", sceneCount = 6, researchBrief = "" , characters = [], productionProfile, imageSourceMode, visualMotionMode }) => {
|
|
533
581
|
const visualSettings = resolveVisualSettings(null, { productionProfile, imageSourceMode, visualMotionMode });
|
|
582
|
+
return runStudioPlan(config, api, 'commerce',
|
|
583
|
+
{ ...visualSettings, productionProfile, productName, productImagePaths: productImagePaths.map(file => path.resolve(file)),
|
|
584
|
+
modelImagePath: modelImagePath ? path.resolve(modelImagePath) : '', description, targetCustomer, tone, categoryId, sceneCount, researchBrief, characters },
|
|
585
|
+
() => {
|
|
534
586
|
// 확장자 검증(임의 파일 업로드 차단) + 합산 크기 예산(서버 413 사전 차단).
|
|
535
587
|
for (const filePath of productImagePaths) assertAllowedInputFile(filePath, ALLOWED_IMAGE_EXTS, { kind: "이미지" });
|
|
536
588
|
if (modelImagePath) assertAllowedInputFile(modelImagePath, ALLOWED_IMAGE_EXTS, { kind: "모델 이미지" });
|
|
537
589
|
assertEncodedBudget([...productImagePaths, modelImagePath], { context: "제품/모델 사진" });
|
|
538
590
|
|
|
539
|
-
|
|
540
|
-
body: {
|
|
591
|
+
return {
|
|
541
592
|
...visualSettings, productionProfile,
|
|
542
593
|
categoryId,
|
|
543
594
|
description,
|
|
@@ -552,25 +603,27 @@ export function registerCloudTools(server, config, api) {
|
|
|
552
603
|
skillOverride: null,
|
|
553
604
|
targetCustomer,
|
|
554
605
|
tone,
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
timeoutMs: 300_000,
|
|
558
|
-
});
|
|
606
|
+
};
|
|
607
|
+
}, payload => {
|
|
559
608
|
const productionPath = saveProductionContext(config.stateDir, {
|
|
609
|
+
...studioPlanningContext(payload),
|
|
560
610
|
topic: productName, visualSettings, autoCompose: productionProfile?.autoComposeEnabled ?? true,
|
|
561
611
|
productionProfile: payload?.productionProfile || productionProfile,
|
|
562
612
|
tagline: payload?.tagline, narrationScript: payload?.narrationScript,
|
|
563
613
|
scenes: trimPlanScenes(payload?.scenes), featureKey: "commerceVideo",
|
|
564
614
|
});
|
|
565
615
|
return jsonResult({
|
|
616
|
+
...studioPlanReceipt(payload),
|
|
566
617
|
productionPath, ...visualSettings, productionProfile: payload?.productionProfile || productionProfile,
|
|
567
618
|
analysis: trimAnalysis(payload?.analysis),
|
|
619
|
+
charactersUsed: payload?.charactersUsed,
|
|
568
620
|
ctaText: payload?.ctaText,
|
|
569
621
|
ok: payload?.ok,
|
|
570
622
|
scenes: trimPlanScenes(payload?.scenes),
|
|
571
623
|
skill: payload?.skill,
|
|
572
624
|
tagline: payload?.tagline,
|
|
573
625
|
});
|
|
626
|
+
});
|
|
574
627
|
}),
|
|
575
628
|
);
|
|
576
629
|
|
|
@@ -756,9 +809,10 @@ export function registerCloudTools(server, config, api) {
|
|
|
756
809
|
|
|
757
810
|
server.tool(
|
|
758
811
|
"tts_narration",
|
|
759
|
-
"내레이션 TTS
|
|
812
|
+
"내레이션 TTS. 자동 순서 Typecast→ElevenLabs→Supertone→Edge→Supertonic2. 공유 모드에서는 작업 ID를 반환하며 finish_speech_generation으로 PC 파일·제작 핸들을 완성합니다. 제작 핸들이 없으면 requestId 필수. 공유 모드의 공급자 폴백은 지원하지 않습니다. 단일 서버에서만 확정된 일시 거절에 허용된 폴백을 사용하며 결과 불명 시 재합성하지 않습니다.",
|
|
760
813
|
{
|
|
761
814
|
...productionFields,
|
|
815
|
+
requestId: z.string().regex(/^[a-zA-Z0-9_-]{16,80}$/u).optional().describe("공유 모드에서 동일 요청에 유지할 ID. 제작 핸들이 있으면 자동 결정. 의도적인 새 합성만 새 ID를 지정하세요."),
|
|
762
816
|
text: z.string().optional().describe("생략 시 productionPath의 기획 대본"),
|
|
763
817
|
...ttsFallbackFields,
|
|
764
818
|
provider: z.enum(["auto", ...TTS_PROVIDERS]).optional().describe("기본 자동: Typecast→ElevenLabs→Supertone→Edge→Supertonic2. 명시 선택/저장된 보이스는 유지"),
|
|
@@ -767,11 +821,15 @@ export function registerCloudTools(server, config, api) {
|
|
|
767
821
|
stability: z.number().min(0).max(1).optional(), style: z.number().min(0).max(1).optional(),
|
|
768
822
|
modelId: z.string().optional(), similarityBoost: z.number().min(0).max(1).optional(), speakerBoost: z.boolean().optional(),
|
|
769
823
|
},
|
|
770
|
-
wrapCloudHandler(config, async ({ text, provider, voice = "", speed, productionPath, productionProfile, stability, style, modelId, similarityBoost, speakerBoost, fallbackVoices, allowProviderFallback }) => {
|
|
824
|
+
wrapCloudHandler(config, async ({ requestId, text, provider, voice = "", speed, productionPath, productionProfile, stability, style, modelId, similarityBoost, speakerBoost, fallbackVoices, allowProviderFallback }) => {
|
|
771
825
|
const context = readProductionContext(config.stateDir, productionPath);
|
|
826
|
+
const sharedSpeech = await trySharedSpeechProduction(api, config, { context, args: {
|
|
827
|
+
requestId, text, provider, voice, speed, productionPath, productionProfile, stability, style, modelId, similarityBoost, speakerBoost, fallbackVoices, allowProviderFallback,
|
|
828
|
+
} });
|
|
829
|
+
if (sharedSpeech?.result) return speechProductionResult(sharedSpeech.result);
|
|
772
830
|
productionProfile = context?.productionProfile || productionProfile;
|
|
773
831
|
text ??= context?.narrationScript;
|
|
774
|
-
const selection = await resolveTtsSelection(api, config, { provider, context });
|
|
832
|
+
const selection = sharedSpeech?.legacySelection || await resolveTtsSelection(api, config, { provider, context });
|
|
775
833
|
provider = selection.provider;
|
|
776
834
|
if (!voice && context?.voice?.provider && context.voice.provider !== provider) return textResult("보이스 공급자가 다릅니다. 선택한 provider용 voice를 다시 선정하세요.", { isError: true });
|
|
777
835
|
voice ||= context?.voice?.voiceId || "";
|
|
@@ -1278,7 +1336,7 @@ export function registerCloudTools(server, config, api) {
|
|
|
1278
1336
|
// ── 뮤직비디오 기획 ──────────────────────────────────────────────────────────
|
|
1279
1337
|
server.tool(
|
|
1280
1338
|
"plan_music_video",
|
|
1281
|
-
"뮤직비디오 씬 플랜을 생성합니다. suno_music_download로 받은 곡의 가사·길이(durationSec)를 입력하세요. 결과 scenes의 imagePrompt는 generate_scene_image로 이어집니다.",
|
|
1339
|
+
"뮤직비디오 씬 플랜을 생성합니다. suno_music_download로 받은 곡의 가사·길이(durationSec)를 입력하세요. 결과 scenes의 imagePrompt는 generate_scene_image로 이어집니다. 공유 모드에서는 같은 입력 재연결은 기존 작업만 조회하며 비용 영수증과 음악 구간을 보존합니다. 접수 불명 작업은 자동 재제출하지 않습니다.",
|
|
1282
1340
|
{
|
|
1283
1341
|
...visualProductionFields,
|
|
1284
1342
|
productionProfile: productionFields.productionProfile,
|
|
@@ -1303,8 +1361,9 @@ export function registerCloudTools(server, config, api) {
|
|
|
1303
1361
|
return textResult("lyrics를 입력하거나 instrumental=true로 지정하세요.", { isError: true });
|
|
1304
1362
|
}
|
|
1305
1363
|
const visualSettings = resolveVisualSettings(null, { productionProfile, imageSourceMode, visualMotionMode });
|
|
1306
|
-
|
|
1307
|
-
|
|
1364
|
+
return runStudioPlan(config, api, 'music',
|
|
1365
|
+
{ ...visualSettings, productionProfile, durationSec, lyrics, conceptBrief, instrumental, sceneSeconds, splitMode, videoStylePreset, aspectRatio, characters },
|
|
1366
|
+
() => ({
|
|
1308
1367
|
...visualSettings, productionProfile,
|
|
1309
1368
|
alignedLines: [],
|
|
1310
1369
|
aspectRatio,
|
|
@@ -1318,19 +1377,19 @@ export function registerCloudTools(server, config, api) {
|
|
|
1318
1377
|
songMeta: {},
|
|
1319
1378
|
splitMode,
|
|
1320
1379
|
videoStylePreset,
|
|
1321
|
-
},
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
});
|
|
1325
|
-
// usage/원장 이벤트 등 컨텍스트 낭비 필드는 제거하고 기획 본문만 돌려준다.
|
|
1326
|
-
const { accountCostEvents, providerUsage, usage, ...plan } = payload && typeof payload === "object" ? payload : {};
|
|
1380
|
+
}), payload => {
|
|
1381
|
+
// Keep settled public receipts for recovery; omit only raw provider detail.
|
|
1382
|
+
const { providerUsage, ...plan } = payload && typeof payload === "object" ? payload : {};
|
|
1327
1383
|
const scenes = trimPlanScenes(plan.scenes);
|
|
1328
1384
|
const productionPath = saveProductionContext(config.stateDir, {
|
|
1385
|
+
...studioPlanningContext(payload),
|
|
1329
1386
|
topic: conceptBrief, visualSettings, autoCompose: productionProfile?.autoComposeEnabled ?? true,
|
|
1330
1387
|
productionProfile: plan.productionProfile || productionProfile,
|
|
1388
|
+
musicPlan: { structure: plan.structure, sceneInputs: plan.sceneInputs, splitSource: plan.splitSource, inferredVibe: plan.inferredVibe },
|
|
1331
1389
|
scenes, featureKey: "musicVideo",
|
|
1332
1390
|
});
|
|
1333
1391
|
return jsonResult({ ...plan, scenes, ...visualSettings, productionPath });
|
|
1392
|
+
});
|
|
1334
1393
|
}),
|
|
1335
1394
|
);
|
|
1336
1395
|
|
|
@@ -1422,10 +1481,18 @@ export function registerCloudTools(server, config, api) {
|
|
|
1422
1481
|
);
|
|
1423
1482
|
|
|
1424
1483
|
// ── 짜집기 렌더 준비 — 컷 세트 → TTS + 타임라인 페이로드 ────────────────────
|
|
1484
|
+
server.tool("continue_narrated_timeline",
|
|
1485
|
+
"공유 리메이크·리뷰 나레이션을 이어갑니다. 기존 행은 조회만 하고, 완료 확인 후 아직 제출하지 않은 행을 호출당 최대 1건 유료 합성합니다. 전체 행 비용에 동의한 경우에만 사용하세요. 실패·결과 불명 행은 자동 재합성하지 않습니다. 모두 완료되면 기존 음성으로 타임라인을 조립합니다.",
|
|
1486
|
+
{ operationId: z.string().regex(/^[A-Za-z0-9_-]{16,80}$/u) },
|
|
1487
|
+
wrapCloudHandler(config, async ({ operationId }) => {
|
|
1488
|
+
const value = await continueNarratedTimeline(api, config, operationId);
|
|
1489
|
+
return { ...jsonResult(value), ...(!["pending", "completed"].includes(value.status) ? { isError: true } : {}) };
|
|
1490
|
+
}));
|
|
1425
1491
|
server.tool(
|
|
1426
1492
|
"prepare_remake_timeline",
|
|
1427
1493
|
"짜집기(리메이크) 렌더 준비 — analyze_edit_points 의 sceneCatalog 에서 고른 컷들로 구성한 컷 세트를 서버에 보내 TTS 합성 + 렌더 타임라인을 만듭니다. 반환된 payloadPath 를 render_start 에 넘기면 이 PC 에서 원본 다운로드 + 렌더가 수행됩니다. [N]/[SN] 행의 audioContent 는 유료 TTS 로 합성되므로 실행 전 사용자 확인을 받으세요.",
|
|
1428
1494
|
{
|
|
1495
|
+
requestId: z.string().regex(/^[A-Za-z0-9_-]{16,80}$/u).optional().describe("공유 실행에서는 필수인 고정 요청 ID. 중단 후 같은 operationId로 continue_narrated_timeline을 호출하세요. 전체 행 TTS 비용에 동의한 뒤 시작하세요."),
|
|
1429
1496
|
title: z.string().optional().describe("영상 상단 타이틀 텍스트 (없으면 생략)"),
|
|
1430
1497
|
sourceVideos: z.array(z.object({
|
|
1431
1498
|
id: z.string().describe("YouTube video id (11자)"),
|
|
@@ -1452,12 +1519,17 @@ export function registerCloudTools(server, config, api) {
|
|
|
1452
1519
|
aspectRatio: z.enum(["9:16", "16:9", "1:1"]).optional().describe("기본 9:16"),
|
|
1453
1520
|
subtitleCropMode: z.enum(["off", "always", "auto"]).optional().describe("원본 박힌 자막 하단 크롭 — auto 는 burnedSubtitle.present 소스만"),
|
|
1454
1521
|
},
|
|
1455
|
-
wrapCloudHandler(config, async ({ title = "", sourceVideos, rows, provider, voiceId = "", aspectRatio = "9:16", subtitleCropMode = "off" }) => {
|
|
1522
|
+
wrapCloudHandler(config, async ({ requestId, title = "", sourceVideos, rows, provider, voiceId = "", aspectRatio = "9:16", subtitleCropMode = "off" }) => {
|
|
1456
1523
|
const sourceIds = new Set(sourceVideos.map((source) => source.id));
|
|
1457
1524
|
const badRef = rows.flatMap((row) => row.clipRefs).find((ref) => !sourceIds.has(ref.videoId));
|
|
1458
1525
|
if (badRef) {
|
|
1459
1526
|
return textResult(`clipRefs 의 videoId "${badRef.videoId}" 가 sourceVideos 에 없습니다.`, { isError: true });
|
|
1460
1527
|
}
|
|
1528
|
+
const version = { id: "mcp-remake", title, sourceVideos, rows: rows.map(row => ({ ...row,
|
|
1529
|
+
clipRefs: row.clipRefs.map(ref => ({ ...ref, durationSec: Math.max(0.3, (ref.endMs - ref.startMs) / 1000) })) })) };
|
|
1530
|
+
const shared = await trySharedNarrationTimeline(api, config, { kind: "remake", requestId, provider, voiceId,
|
|
1531
|
+
body: { version, aspectRatio, subtitleCropMode } });
|
|
1532
|
+
if (shared) return { ...jsonResult(shared), ...(!["pending", "completed"].includes(shared.status) ? { isError: true } : {}) };
|
|
1461
1533
|
const hasNarration = rows.some((row) => ["N", "SN"].includes(row.mode) && row.audioContent?.trim());
|
|
1462
1534
|
const selection = hasNarration ? await resolveTtsSelection(api, config, { provider }) : { provider: "typecast", executionTarget: "server" };
|
|
1463
1535
|
provider = selection.provider;
|
|
@@ -1472,18 +1544,6 @@ export function registerCloudTools(server, config, api) {
|
|
|
1472
1544
|
}
|
|
1473
1545
|
preSynthesizedNarrations = { narrations, attempted: Object.keys(narrations).length, succeeded: Object.keys(narrations).length, lastError: "" };
|
|
1474
1546
|
}
|
|
1475
|
-
const version = {
|
|
1476
|
-
id: "mcp-remake",
|
|
1477
|
-
title,
|
|
1478
|
-
sourceVideos,
|
|
1479
|
-
rows: rows.map((row) => ({
|
|
1480
|
-
...row,
|
|
1481
|
-
clipRefs: row.clipRefs.map((ref) => ({
|
|
1482
|
-
...ref,
|
|
1483
|
-
durationSec: Math.max(0.3, (ref.endMs - ref.startMs) / 1000),
|
|
1484
|
-
})),
|
|
1485
|
-
})),
|
|
1486
|
-
};
|
|
1487
1547
|
const payload = await api.request("/api/tracker/remake/prepare", {
|
|
1488
1548
|
body: {
|
|
1489
1549
|
aspectRatio,
|
|
@@ -1518,9 +1578,10 @@ export function registerCloudTools(server, config, api) {
|
|
|
1518
1578
|
// ── 시간 동기 자막 TTS — 쇼츠 하단자막용 ─────────────────────────────────────
|
|
1519
1579
|
server.tool(
|
|
1520
1580
|
"tts_narration_with_captions",
|
|
1521
|
-
"TTS 오디오와
|
|
1581
|
+
"TTS 오디오와 자막. 순서 Typecast→ElevenLabs→Supertone→Edge→Supertonic2. 공유 모드에서는 ID를 반환하며 finish_speech_generation으로 PC 파일·제작 핸들을 완성합니다. 제작 핸들 없으면 requestId 필수, 공유 공급자 폴백 없음. ElevenLabs는 실제 문자 타이밍, 나머지는 오디오 길이 기반 추정 자막이며 추가 STT 과금은 없습니다.",
|
|
1522
1582
|
{
|
|
1523
1583
|
...productionFields,
|
|
1584
|
+
requestId: z.string().regex(/^[a-zA-Z0-9_-]{16,80}$/u).optional().describe("공유 모드의 고정 요청 ID. 제작 핸들이 없으면 필수. 같은 ID로 조회하며 새 유료 요청을 만들지 마세요."),
|
|
1524
1585
|
text: z.string().optional().describe("생략 시 제작 핸들의 기획 대본 그대로 합성. 사실·인용을 바꾸거나 뉴스체로 재작성하지 마세요."),
|
|
1525
1586
|
...ttsFallbackFields,
|
|
1526
1587
|
provider: z.enum(["auto", ...TTS_PROVIDERS]).optional().describe("기본 자동 선택 또는 제작 핸들에 선정된 공급자"),
|
|
@@ -1529,11 +1590,15 @@ export function registerCloudTools(server, config, api) {
|
|
|
1529
1590
|
modelId: z.string().optional(), stability: z.number().min(0).max(1).optional(),
|
|
1530
1591
|
style: z.number().min(0).max(1).optional(), similarityBoost: z.number().min(0).max(1).optional(), speakerBoost: z.boolean().optional(),
|
|
1531
1592
|
},
|
|
1532
|
-
wrapCloudHandler(config, async ({ text, provider, voice = "", speed, productionPath, productionProfile, modelId, stability, style, similarityBoost, speakerBoost, fallbackVoices, allowProviderFallback }) => {
|
|
1593
|
+
wrapCloudHandler(config, async ({ requestId, text, provider, voice = "", speed, productionPath, productionProfile, modelId, stability, style, similarityBoost, speakerBoost, fallbackVoices, allowProviderFallback }) => {
|
|
1533
1594
|
const context = readProductionContext(config.stateDir, productionPath);
|
|
1595
|
+
const sharedSpeech = await trySharedSpeechProduction(api, config, { context, withCaptions: true, args: {
|
|
1596
|
+
requestId, text, provider, voice, speed, productionPath, productionProfile, modelId, stability, style, similarityBoost, speakerBoost, fallbackVoices, allowProviderFallback,
|
|
1597
|
+
} });
|
|
1598
|
+
if (sharedSpeech?.result) return speechProductionResult(sharedSpeech.result);
|
|
1534
1599
|
productionProfile = context?.productionProfile || productionProfile;
|
|
1535
1600
|
text ??= context?.narrationScript;
|
|
1536
|
-
const selection = await resolveTtsSelection(api, config, { provider, context });
|
|
1601
|
+
const selection = sharedSpeech?.legacySelection || await resolveTtsSelection(api, config, { provider, context });
|
|
1537
1602
|
provider = selection.provider;
|
|
1538
1603
|
if (!voice && context?.voice?.provider && context.voice.provider !== provider) {
|
|
1539
1604
|
return textResult("보이스 공급자가 다릅니다. 선택한 provider용 voice를 다시 선정하세요.", { isError: true });
|
|
@@ -1647,18 +1712,24 @@ export function registerCloudTools(server, config, api) {
|
|
|
1647
1712
|
sampleVideoUrl: z.string().optional().describe("사용자가 샘플로 지정한 유튜브 URL 또는 11자 영상 ID"),
|
|
1648
1713
|
provider: z.enum(["auto", ...TTS_PROVIDERS]).optional().describe("기본 자동: Typecast→ElevenLabs→Supertone→Edge→Supertonic2. 키가 없으면 다음 후보, 합성 실패 후 자동 재과금 없음"),
|
|
1649
1714
|
sampleVoiceHints: z.string().optional().describe("사용자가 말한 보이스 요구 (예: 차분한 중년 남성)"),
|
|
1715
|
+
requestId: z.string().uuid().optional().describe("새 추천을 명시적으로 요청할 때만 새 UUID를 지정합니다. 복구·재연결에는 같은 값을 유지하며 자동 변경하지 않습니다."),
|
|
1650
1716
|
},
|
|
1651
|
-
wrapCloudHandler(config, async ({ topic = "", scriptExcerpt = "", sampleVideoUrl = "", provider, sampleVoiceHints = "", productionPath, productionProfile }) => {
|
|
1717
|
+
wrapCloudHandler(config, async ({ topic = "", scriptExcerpt = "", sampleVideoUrl = "", provider, sampleVoiceHints = "", productionPath, productionProfile, requestId }) => {
|
|
1718
|
+
const requestedProvider = provider || 'auto';
|
|
1652
1719
|
const context = readProductionContext(config.stateDir, productionPath);
|
|
1653
1720
|
productionProfile = context?.productionProfile || productionProfile;
|
|
1654
1721
|
const selection = await resolveTtsSelection(api, config, { provider, context });
|
|
1655
1722
|
provider = selection.provider;
|
|
1656
1723
|
topic ||= context?.topic || "";
|
|
1657
1724
|
scriptExcerpt ||= context?.narrationScript?.slice(0, 2000) || "";
|
|
1725
|
+
const casting = createVoiceRecommendationClient(config, api, { requestedProvider, topic, scriptExcerpt, sampleVideoUrl: canonicalYoutubeWatchUrl(sampleVideoUrl) || sampleVideoUrl, sampleVoiceHints, productionProfile, requestId });
|
|
1726
|
+
const recovered = await casting.recover();
|
|
1727
|
+
const castingMode = recovered ? 'queue-v2' : await casting.readCapability();
|
|
1728
|
+
if (!['queue-v2', 'legacy-sync'].includes(castingMode)) throw Error('현재 서버에서 보이스 추천의 안전한 공유 실행을 사용할 수 없습니다.');
|
|
1658
1729
|
const notes = [];
|
|
1659
1730
|
let sampleAudioBase64 = "";
|
|
1660
1731
|
const rawSample = String(sampleVideoUrl || "").trim();
|
|
1661
|
-
if (rawSample) {
|
|
1732
|
+
if (rawSample && !recovered) {
|
|
1662
1733
|
// 모바일/뮤직/shorts/youtu.be 변형을 표준 watch URL 로 정규화 — 컴패니언의
|
|
1663
1734
|
// 유튜브 전용 처리(봇 우회)가 표준 형태에서만 발동한다.
|
|
1664
1735
|
const watchUrl = canonicalYoutubeWatchUrl(rawSample);
|
|
@@ -1675,18 +1746,27 @@ export function registerCloudTools(server, config, api) {
|
|
|
1675
1746
|
);
|
|
1676
1747
|
}
|
|
1677
1748
|
}
|
|
1678
|
-
if (!topic && !scriptExcerpt && !sampleVoiceHints && !sampleAudioBase64) {
|
|
1749
|
+
if (!recovered && !topic && !scriptExcerpt && !sampleVoiceHints && !sampleAudioBase64) {
|
|
1679
1750
|
// 샘플만 줬는데 추출이 실패한 경우: 누락 안내가 아니라 실패 사유를 그대로 전달한다.
|
|
1680
1751
|
if (notes.length) {
|
|
1681
1752
|
return textResult(`${notes.join("\n")}\n주제(topic)를 함께 주면 샘플 없이도 추천할 수 있습니다.`, { isError: true });
|
|
1682
1753
|
}
|
|
1683
1754
|
return textResult("topic, scriptExcerpt, sampleVoiceHints, sampleVideoUrl 중 하나는 필요합니다.", { isError: true });
|
|
1684
1755
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1756
|
+
let generated = recovered;
|
|
1757
|
+
let payload;
|
|
1758
|
+
if (!generated && castingMode === 'legacy-sync') payload = await casting.legacy({ provider, sampleAudioBase64, sampleVoiceHints, scriptExcerpt, topic, productionProfile,
|
|
1759
|
+
...(selection.localVoices ? { localVoiceCatalog: selection.localVoices } : {}) });
|
|
1760
|
+
else {
|
|
1761
|
+
if (!generated) {
|
|
1762
|
+
const voices = selection.localVoices || (await api.request(`/api/tracker/tts/${provider}/voices`, { method: 'GET', timeoutMs: 30000 })).voices;
|
|
1763
|
+
const voiceCatalog = compactVoiceCatalog(voices, provider);
|
|
1764
|
+
generated = await casting.run({ ttsProvider: provider, voiceCatalog, sampleAudioBase64, sampleVoiceHints, scriptExcerpt, topic,
|
|
1765
|
+
...(productionProfile ? { productionProfile } : {}) });
|
|
1766
|
+
}
|
|
1767
|
+
if (generated.voiceRecommendation?.ttsProvider !== provider) throw Error('VOICE_RECOMMENDATION_PROVIDER_MISMATCH');
|
|
1768
|
+
payload = { ...generated.voiceRecommendation, provider, ok: true };
|
|
1769
|
+
}
|
|
1690
1770
|
if (payload?.ok === false) {
|
|
1691
1771
|
return textResult(
|
|
1692
1772
|
`보이스 추천 실패: ${payload?.error || "알 수 없는 오류"}\n${payload?.guidance || ""}`.trim(),
|
|
@@ -1700,8 +1780,9 @@ export function registerCloudTools(server, config, api) {
|
|
|
1700
1780
|
notes: [...notes, ...(Array.isArray(payload?.notes) ? payload.notes : [])],
|
|
1701
1781
|
provider: payload?.provider || provider,
|
|
1702
1782
|
recommendations: Array.isArray(payload?.recommendations) ? payload.recommendations : [],
|
|
1703
|
-
sampleAnalyzed: Boolean(
|
|
1783
|
+
sampleAnalyzed: Boolean(payload?.sampleVoiceProfile),
|
|
1704
1784
|
sampleVoiceProfile: payload?.sampleVoiceProfile || null,
|
|
1785
|
+
...(generated ? { backgroundJobId: generated.backgroundJobId, usage: generated.usage, accountCostEvents: generated.accountCostEvents, recovered: Boolean(recovered), paidReplayAllowed: false } : {}),
|
|
1705
1786
|
});
|
|
1706
1787
|
}),
|
|
1707
1788
|
);
|
|
@@ -1713,6 +1794,10 @@ export function registerCloudTools(server, config, api) {
|
|
|
1713
1794
|
videoUrls: z.array(z.string()).min(1).max(5).describe("표본 유튜브 URL 또는 11자 ID (조회수 상위 영상 권장)"),
|
|
1714
1795
|
},
|
|
1715
1796
|
wrapCloudHandler(config, async ({ videoUrls }) => {
|
|
1797
|
+
const durable = createChannelMeasurementClient(config, api, videoUrls);
|
|
1798
|
+
const restored = await durable.recover();
|
|
1799
|
+
if (restored) return jsonResult({ fingerprint: restored.fingerprint, usage: restored.usage, accountCostEvents: restored.accountCostEvents, backgroundJobId: restored.backgroundJobId, recovered: true, paidReplayAllowed: false });
|
|
1800
|
+
await durable.readCapability();
|
|
1716
1801
|
const specs = [];
|
|
1717
1802
|
const notes = [];
|
|
1718
1803
|
// 라우드니스·무음은 여기서 재고 "숫자만" 보낸다. 오디오를 서버로 올리면 표본 3편만
|
|
@@ -1783,16 +1868,16 @@ export function registerCloudTools(server, config, api) {
|
|
|
1783
1868
|
if (!specs.length) {
|
|
1784
1869
|
return textResult(`표본 실측에 모두 실패했습니다. 컴패니언 앱 실행 여부를 확인하세요.\n${notes.join("\n")}`, { isError: true });
|
|
1785
1870
|
}
|
|
1786
|
-
const payload = await
|
|
1787
|
-
body: { measuredAt: new Date().toISOString(), videos: specs },
|
|
1788
|
-
method: "POST",
|
|
1789
|
-
timeoutMs: 300_000,
|
|
1790
|
-
});
|
|
1871
|
+
const payload = await durable.run({ measuredAt: new Date().toISOString(), videos: specs });
|
|
1791
1872
|
if (payload?.ok === false || !payload?.fingerprint) {
|
|
1792
1873
|
return textResult(`지문 집계 실패: ${payload?.error || "알 수 없는 오류"}`, { isError: true });
|
|
1793
1874
|
}
|
|
1794
1875
|
return jsonResult({
|
|
1795
1876
|
fingerprint: payload.fingerprint,
|
|
1877
|
+
usage: payload.usage,
|
|
1878
|
+
accountCostEvents: payload.accountCostEvents,
|
|
1879
|
+
backgroundJobId: payload.backgroundJobId,
|
|
1880
|
+
...(payload.kind === "durable-channel-v2" ? { paidReplayAllowed: false } : {}),
|
|
1796
1881
|
next: "이 fingerprint 객체를 plan_shorts_video·stitch_timeline·write_script 의 channelFingerprint 로 그대로 넘기세요 — 길이·컷 리듬·말끝·자막 위치·BGM 이 이 규격을 따릅니다.",
|
|
1797
1882
|
notes: notes.length ? notes : undefined,
|
|
1798
1883
|
sampleCount: specs.length,
|
package/lib/config.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
// 프록시 버전 — 서버가 X-AImakeAll-MCP-Version 으로 하한을 강제(426)할 수 있다.
|
|
5
|
-
export const MCP_PROXY_VERSION = "0.14.
|
|
5
|
+
export const MCP_PROXY_VERSION = "0.14.5";
|
|
6
6
|
|
|
7
7
|
export const DEFAULT_API_BASE = "https://aimakeall.com";
|
|
8
8
|
export const DEFAULT_COMPANION_URL = "http://127.0.0.1:9876";
|