@yoonion/mimi-seed-mcp 0.18.0 → 0.19.0
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 +1 -1
- package/assets/agent-guide.md +7 -6
- package/dist/appstore/auth.js +4 -1
- package/dist/auth/bigquery-auth.d.ts +1 -2
- package/dist/auth/bigquery-auth.js +2 -2
- package/dist/auth/bigquery-setup-cli.js +2 -2
- package/dist/auth/playstore-auth.d.ts +1 -1
- package/dist/auth/playstore-auth.js +2 -2
- package/dist/lib/google-auth-lite.d.ts +3 -0
- package/dist/lib/google-auth-lite.js +29 -0
- package/dist/lib/googleapis-lite.d.ts +17 -39
- package/dist/lib/googleapis-lite.js +81 -32
- package/dist/playstore/financials.js +2 -2
- package/dist/playstore/tools.d.ts +1 -2
- package/dist/playstore/tools.js +2 -2
- package/dist/registers/video.js +38 -5
- package/dist/video/captions.d.ts +34 -0
- package/dist/video/captions.js +98 -0
- package/dist/video/project.d.ts +19 -2
- package/dist/video/project.js +45 -2
- package/dist/video/render.d.ts +1 -6
- package/dist/video/render.js +9 -38
- package/dist/video/schemas.d.ts +87 -0
- package/dist/video/schemas.js +11 -0
- package/dist/video/types.d.ts +10 -0
- package/package.json +1 -1
- package/tool-manifest.json +2 -1
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|
|
99
99
|
| Android 서명 | 3 | `android_signing_setup` / `android_generate_keystore` / `jenkins_upload_playstore_sa` |
|
|
100
100
|
| 인증 | 4 | `mimi_seed_status` / `mimi_seed_auth_start` / `mimi_seed_auth_status` / `mimi_seed_remote_sync_credentials` |
|
|
101
101
|
| AI (Claude) | 2 | `generate_release_notes_from_commits` / `generate_review_reply` |
|
|
102
|
-
| 영상 제작·YouTube |
|
|
102
|
+
| 영상 제작·YouTube | 15 | `youtube_upload_video` / `youtube_get_video_status` / `youtube_update_video_privacy` / `video_plan_from_story` / `video_render` |
|
|
103
103
|
|
|
104
104
|
> 인앱 결제(IAP·구독) 도구는 위 Play Store·App Store 카운트에 포함됩니다 — `appstore_create_inapp_purchase` · `appstore_update_product_review_note` · `appstore_upload_product_review_screenshot` 등.
|
|
105
105
|
> 전체 카탈로그(항상 최신): [`docs/domain/tool-catalog.md`](../../docs/domain/tool-catalog.md)
|
package/assets/agent-guide.md
CHANGED
|
@@ -80,7 +80,7 @@ you can paste. Pick the row for the job; batching two rows in one `select:` call
|
|
|
80
80
|
| CI (GitHub/GitLab) | `select:ci_save_config,ci_list_workflows,ci_trigger_build,ci_get_build_status,ci_list_recent_builds,ci_cancel_build` |
|
|
81
81
|
| Android signing / keystore | `select:android_signing_setup,android_generate_keystore,jenkins_upload_keystore,jenkins_upload_playstore_sa` |
|
|
82
82
|
| Service account end-to-end | `select:iam_list_service_accounts,iam_create_service_account,iam_list_keys,iam_create_key,iam_add_iam_policy_binding,setup_playstore_connection,playstore_register_service_account,playstore_verify_service_account,playstore_list_service_accounts,playstore_delete_service_account` |
|
|
83
|
-
| Story → researched video | `select:video_plan_from_story,video_research_youtube,video_search_stock_assets,video_synthesize_research,video_download_stock_assets,video_generate_image,video_add_local_asset,video_build_timeline,video_render,video_job_status,video_validate` |
|
|
83
|
+
| Story → researched video | `select:video_save_plan,video_plan_from_story,video_research_youtube,video_search_stock_assets,video_synthesize_research,video_download_stock_assets,video_generate_image,video_add_local_asset,video_build_timeline,video_render,video_job_status,video_validate` |
|
|
84
84
|
| YouTube upload / publish | `select:youtube_upload_video,youtube_get_video_status,youtube_update_video_privacy,mimi_seed_auth_start` |
|
|
85
85
|
|
|
86
86
|
---
|
|
@@ -191,7 +191,7 @@ per-domain inventory is [`docs/domain/tool-catalog.md`](domain/tool-catalog.md).
|
|
|
191
191
|
| **TikTok Business** | `tiktok_business_auth_status` · `tiktok_business_plan_video_post` · `tiktok_business_publish_video` · `tiktok_business_get_publish_status` |
|
|
192
192
|
| **Checks** | `playstore_check_submission_risks` · `appstore_check_submission_risks` · `screenshot_validate` · `release_status` |
|
|
193
193
|
| **AI / Auth** | `generate_release_notes_from_commits` · `generate_review_reply` · `mimi_seed_status` · `mimi_seed_auth_start` · `mimi_seed_auth_status` · `mimi_seed_remote_sync_credentials` |
|
|
194
|
-
| **Video production** | `youtube_upload_video` · `youtube_get_video_status` · `youtube_update_video_privacy` · `video_plan_from_story` · `video_research_youtube` · `video_search_stock_assets` · `video_synthesize_research` · `video_generate_image` · `video_build_timeline` · `video_render` · `video_job_status` · `video_validate` |
|
|
194
|
+
| **Video production** | `youtube_upload_video` · `youtube_get_video_status` · `youtube_update_video_privacy` · `video_save_plan` · `video_plan_from_story` · `video_research_youtube` · `video_search_stock_assets` · `video_synthesize_research` · `video_generate_image` · `video_build_timeline` · `video_render` · `video_job_status` · `video_validate` |
|
|
195
195
|
|
|
196
196
|
---
|
|
197
197
|
|
|
@@ -218,15 +218,16 @@ For visual production, use the bundled `video-create-publish` skill. It requires
|
|
|
218
218
|
aspect-ratio-specific human-safe crops, video-native motion, an original-resolution frame review, and a saved
|
|
219
219
|
contact sheet; codec validation alone is not a quality pass.
|
|
220
220
|
|
|
221
|
-
1. `
|
|
221
|
+
1. `video_save_plan` — save an agent-authored storyboard (no API key; free-path default). `video_plan_from_story` is the metered alternative when `ANTHROPIC_API_KEY` is configured.
|
|
222
222
|
2. `video_research_youtube` — collect reference-only metadata; never treat a result as a render asset.
|
|
223
223
|
3. `video_search_stock_assets` — find licensed Pexels candidates.
|
|
224
224
|
4. `video_synthesize_research` — combine metadata with any direct human/agent observations. Treat its output as
|
|
225
225
|
metadata-bounded guidance, not proof that the source videos were watched.
|
|
226
226
|
5. Preview then confirm `video_download_stock_assets`; use `video_generate_image(confirm=false)` before any paid
|
|
227
|
-
generation and call it again with `confirm=true` only after approval.
|
|
228
|
-
`
|
|
229
|
-
|
|
227
|
+
generation and call it again with `confirm=true` only after approval. On the free path, generate scene images
|
|
228
|
+
with the local `codex` CLI (ChatGPT subscription) instead and register them — like all user-owned media —
|
|
229
|
+
through `video_add_local_asset` with the ownership/license basis.
|
|
230
|
+
6. `video_build_timeline` — provenance-gated scene assignment. Captions burn in the shorts style (bold outline, `**keyword**` highlight); tune with `captionStyle`.
|
|
230
231
|
7. Preview then confirm `video_render`; poll `video_job_status`, then run `video_validate` on the completed MP4.
|
|
231
232
|
|
|
232
233
|
> **Mimi Seed does not compile app binaries.** It manages metadata, store releases, and
|
package/dist/appstore/auth.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
// jose 는 값 import 하지 않는다 — import 만으로 ~0.6초가 나가고, 이 파일은
|
|
2
|
+
// helpers.ts 를 통해 대부분의 register 에 딸려 들어가 MCP 기동 시간을 그대로 밀어올린다.
|
|
3
|
+
// 실제로 필요한 곳은 generateToken() 하나뿐이라 그 안에서 동적 import 한다.
|
|
2
4
|
import fs from 'node:fs';
|
|
3
5
|
import path from 'node:path';
|
|
4
6
|
import os from 'node:os';
|
|
@@ -41,6 +43,7 @@ function normalizePrivateKey(raw) {
|
|
|
41
43
|
return [header, ...chunks, footer, ''].join('\n');
|
|
42
44
|
}
|
|
43
45
|
export async function generateToken(creds) {
|
|
46
|
+
const { SignJWT, importPKCS8 } = await import('jose');
|
|
44
47
|
const normalizedKey = normalizePrivateKey(creds.privateKey);
|
|
45
48
|
let key;
|
|
46
49
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { newJWT } from '../lib/google-auth-lite.js';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
@@ -55,7 +55,7 @@ export function deleteBigQueryServiceAccountJson() {
|
|
|
55
55
|
return true;
|
|
56
56
|
}
|
|
57
57
|
function makeJwt(sa) {
|
|
58
|
-
return
|
|
58
|
+
return newJWT({ email: sa.client_email, key: sa.private_key, scopes: BQ_SCOPES });
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
61
|
* BigQuery 인증 클라이언트 해석. 우선순위:
|
|
@@ -3,7 +3,7 @@ import readline from 'node:readline';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { saveBigQueryServiceAccountJson, getBigQueryServiceAccountKey, } from './bigquery-auth.js';
|
|
5
5
|
import * as bigquery from '../bigquery/tools.js';
|
|
6
|
-
import {
|
|
6
|
+
import { newJWT } from '../lib/google-auth-lite.js';
|
|
7
7
|
import { resolveLang } from '../lib/lang.js';
|
|
8
8
|
// ko 가 원본이고 en 은 `typeof ko` 를 만족해야 한다 — 키를 빠뜨리면 컴파일이 깨진다.
|
|
9
9
|
const ko = {
|
|
@@ -111,7 +111,7 @@ async function main() {
|
|
|
111
111
|
if (projectId) {
|
|
112
112
|
console.log(M.probing(projectId));
|
|
113
113
|
try {
|
|
114
|
-
const jwt =
|
|
114
|
+
const jwt = newJWT({
|
|
115
115
|
email: parsed.client_email,
|
|
116
116
|
key: parsed.private_key,
|
|
117
117
|
scopes: [
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { newJWT } from '../lib/google-auth-lite.js';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
@@ -106,7 +106,7 @@ export function getServiceAccountClient(packageName) {
|
|
|
106
106
|
return null;
|
|
107
107
|
try {
|
|
108
108
|
const parsed = JSON.parse(json);
|
|
109
|
-
return
|
|
109
|
+
return newJWT({
|
|
110
110
|
email: parsed.client_email,
|
|
111
111
|
key: parsed.private_key,
|
|
112
112
|
// androidpublisher(edits/리스팅 등) + Developer Reporting(vitals 통계). 통계 도구가
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* google-auth-library 지연 로더 — googleapis-lite 와 같은 목적, 같은 기법.
|
|
3
|
+
*
|
|
4
|
+
* `import { JWT } from 'google-auth-library'` 는 그 자체로 ~0.6초다. 문제는 이게
|
|
5
|
+
* helpers.ts → 12개 register 로 전파돼, 도구를 하나도 호출하지 않아도 MCP 기동 때
|
|
6
|
+
* 무조건 지불된다는 점이다. 콜드 캐시에서는 이런 항목들이 합쳐져 기동이 25초까지 튀고,
|
|
7
|
+
* Claude Code 의 기본 5초 MCP 연결 타임아웃을 넘겨 서버가 통째로 등록되지 않는다.
|
|
8
|
+
*
|
|
9
|
+
* google-auth-library 는 `type: module` 도 `exports` 맵도 없는 순수 CJS 라
|
|
10
|
+
* `createRequire` 로 **동기** 로드가 된다. 덕분에 `getServiceAccountClient()` 처럼
|
|
11
|
+
* 이미 동기인 함수를 async 로 바꾸지 않고도 지연 로딩이 가능하다.
|
|
12
|
+
*
|
|
13
|
+
* 규칙:
|
|
14
|
+
* - `JWT` 를 **값으로** 쓰려면 이 파일의 `newJWT()` 를 쓴다.
|
|
15
|
+
* - 타입만 필요하면 `import type { JWT } from 'google-auth-library'` — 타입 import 는
|
|
16
|
+
* 런타임에 지워지므로 기동 비용이 0 이고 그대로 써도 된다.
|
|
17
|
+
* - 다른 파일에서 `import { JWT } from 'google-auth-library'` (값 import) 는 금지.
|
|
18
|
+
*/
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
let cached;
|
|
22
|
+
function lib() {
|
|
23
|
+
cached ??= require('google-auth-library');
|
|
24
|
+
return cached;
|
|
25
|
+
}
|
|
26
|
+
/** `new JWT(opts)` 와 동일. 첫 호출에서만 google-auth-library 를 로드한다. */
|
|
27
|
+
export function newJWT(opts) {
|
|
28
|
+
return new (lib().JWT)(opts);
|
|
29
|
+
}
|
|
@@ -1,43 +1,21 @@
|
|
|
1
|
+
export type { youtube_v3 } from 'googleapis/build/src/apis/youtube/index.js';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* `import { google } from 'googleapis'` 는 import 시점에 400여 개 API 클라이언트를
|
|
5
|
-
* 전부 로드해 그것만으로 ~19초를 쓴다. 그 결과 MCP 서버 기동(21~36초)이 Claude Code 의
|
|
6
|
-
* MCP 연결 타임아웃(30초)을 상습 초과해, 세션에서 mimi-seed 도구가 아예 등록되지 않는
|
|
7
|
-
* 사고가 났다 (2026-07-24). 실제 사용하는 API 만 서브패스로 로드하면 ~1초대다.
|
|
8
|
-
*
|
|
9
|
-
* - 새 Google API 가 필요하면 여기에 import 한 줄 + google 객체에 한 줄 추가한다.
|
|
10
|
-
* - 다른 파일에서 `from 'googleapis'` 값 import 는 금지 — 반드시 이 모듈을 거친다.
|
|
11
|
-
* (googleapis 는 exports map 이 없어 서브패스 import 가 공식적으로 가능하다.)
|
|
12
|
-
* - `auth` 는 AuthPlus 인스턴스라 `google.auth.OAuth2` 등 기존 사용처가 그대로 동작한다.
|
|
3
|
+
* 기존 `google.<api>(...)` 호출부와 100% 동일하게 동작하는 지연 로딩 네임스페이스.
|
|
4
|
+
* 각 프로퍼티는 getter 라서, 실제로 그 API 를 쓰는 도구가 호출될 때까지 아무것도 로드하지 않는다.
|
|
13
5
|
*/
|
|
14
|
-
import { admob } from 'googleapis/build/src/apis/admob/index.js';
|
|
15
|
-
import { analyticsadmin } from 'googleapis/build/src/apis/analyticsadmin/index.js';
|
|
16
|
-
import { analyticsdata } from 'googleapis/build/src/apis/analyticsdata/index.js';
|
|
17
|
-
import { androidpublisher } from 'googleapis/build/src/apis/androidpublisher/index.js';
|
|
18
|
-
import { bigquery } from 'googleapis/build/src/apis/bigquery/index.js';
|
|
19
|
-
import { billingbudgets } from 'googleapis/build/src/apis/billingbudgets/index.js';
|
|
20
|
-
import { cloudbilling } from 'googleapis/build/src/apis/cloudbilling/index.js';
|
|
21
|
-
import { cloudresourcemanager } from 'googleapis/build/src/apis/cloudresourcemanager/index.js';
|
|
22
|
-
import { firebase } from 'googleapis/build/src/apis/firebase/index.js';
|
|
23
|
-
import { iam } from 'googleapis/build/src/apis/iam/index.js';
|
|
24
|
-
import { searchconsole } from 'googleapis/build/src/apis/searchconsole/index.js';
|
|
25
|
-
import { serviceusage } from 'googleapis/build/src/apis/serviceusage/index.js';
|
|
26
|
-
import { youtube } from 'googleapis/build/src/apis/youtube/index.js';
|
|
27
|
-
export type { youtube_v3 } from 'googleapis/build/src/apis/youtube/index.js';
|
|
28
6
|
export declare const google: {
|
|
29
|
-
auth: import("googleapis
|
|
30
|
-
admob: typeof admob;
|
|
31
|
-
analyticsadmin: typeof analyticsadmin;
|
|
32
|
-
analyticsdata: typeof analyticsdata;
|
|
33
|
-
androidpublisher: typeof androidpublisher;
|
|
34
|
-
bigquery: typeof bigquery;
|
|
35
|
-
billingbudgets: typeof billingbudgets;
|
|
36
|
-
cloudbilling: typeof cloudbilling;
|
|
37
|
-
cloudresourcemanager: typeof cloudresourcemanager;
|
|
38
|
-
firebase: typeof firebase;
|
|
39
|
-
iam: typeof iam;
|
|
40
|
-
searchconsole: typeof searchconsole;
|
|
41
|
-
serviceusage: typeof serviceusage;
|
|
42
|
-
youtube: typeof youtube;
|
|
7
|
+
readonly auth: typeof import("googleapis/build/src/apis/firebase/index.js").auth;
|
|
8
|
+
readonly admob: typeof import("googleapis/build/src/apis/admob/index.js").admob;
|
|
9
|
+
readonly analyticsadmin: typeof import("googleapis/build/src/apis/analyticsadmin/index.js").analyticsadmin;
|
|
10
|
+
readonly analyticsdata: typeof import("googleapis/build/src/apis/analyticsdata/index.js").analyticsdata;
|
|
11
|
+
readonly androidpublisher: typeof import("googleapis/build/src/apis/androidpublisher/index.js").androidpublisher;
|
|
12
|
+
readonly bigquery: typeof import("googleapis/build/src/apis/bigquery/index.js").bigquery;
|
|
13
|
+
readonly billingbudgets: typeof import("googleapis/build/src/apis/billingbudgets/index.js").billingbudgets;
|
|
14
|
+
readonly cloudbilling: typeof import("googleapis/build/src/apis/cloudbilling/index.js").cloudbilling;
|
|
15
|
+
readonly cloudresourcemanager: typeof import("googleapis/build/src/apis/cloudresourcemanager/index.js").cloudresourcemanager;
|
|
16
|
+
readonly firebase: typeof import("googleapis/build/src/apis/firebase/index.js").firebase;
|
|
17
|
+
readonly iam: typeof import("googleapis/build/src/apis/iam/index.js").iam;
|
|
18
|
+
readonly searchconsole: typeof import("googleapis/build/src/apis/searchconsole/index.js").searchconsole;
|
|
19
|
+
readonly serviceusage: typeof import("googleapis/build/src/apis/serviceusage/index.js").serviceusage;
|
|
20
|
+
readonly youtube: typeof import("googleapis/build/src/apis/youtube/index.js").youtube;
|
|
43
21
|
};
|
|
@@ -3,40 +3,89 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `import { google } from 'googleapis'` 는 import 시점에 400여 개 API 클라이언트를
|
|
5
5
|
* 전부 로드해 그것만으로 ~19초를 쓴다. 그 결과 MCP 서버 기동(21~36초)이 Claude Code 의
|
|
6
|
-
* MCP 연결
|
|
7
|
-
* 사고가 났다 (2026-07-24). 실제 사용하는 API 만 서브패스로 로드하면 ~1초대다.
|
|
6
|
+
* MCP 연결 타임아웃을 상습 초과해, 세션에서 mimi-seed 도구가 아예 등록되지 않는
|
|
7
|
+
* 사고가 났다 (2026-07-24). 실제 사용하는 API 만 서브패스로 로드하면 ~1.4초대다.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 그런데 그 ~1.4초도 **기동 시점에** 전부 나갔다. 서브패스를 정적 import 하면
|
|
10
|
+
* 도구를 하나도 호출하지 않아도 googleapis 공통 런타임(google-auth-library 등)이
|
|
11
|
+
* 딸려 온다. 콜드 캐시(Windows 실시간 검사 등)에서는 이게 25초까지 튀어
|
|
12
|
+
* 기본 5초 연결 타임아웃을 다시 넘겼다 (2026-08-22).
|
|
13
|
+
*
|
|
14
|
+
* 그래서 지금은 **첫 사용 시점까지 미룬다**:
|
|
15
|
+
* - googleapis 는 `type: module` 도 `exports` 맵도 없는 순수 CJS 라
|
|
16
|
+
* `createRequire` 로 **동기** 로드가 가능하다. 그래서 `google.admob(...)` 같은
|
|
17
|
+
* 기존 호출부를 async 로 바꿀 필요가 전혀 없다 — 소비자 코드는 그대로다.
|
|
18
|
+
* - 타입은 `typeof import(...)` 로 얻는다. 타입 위치의 import 는 런타임에 완전히
|
|
19
|
+
* 지워지므로 기동 비용이 0 이다. 절대 값 import 로 바꾸지 말 것 — 그 순간 이 파일의
|
|
20
|
+
* 존재 이유가 사라진다.
|
|
21
|
+
*
|
|
22
|
+
* 규칙:
|
|
23
|
+
* - 새 Google API 가 필요하면 아래 `google` 객체에 getter 한 줄을 추가한다.
|
|
10
24
|
* - 다른 파일에서 `from 'googleapis'` 값 import 는 금지 — 반드시 이 모듈을 거친다.
|
|
11
|
-
* (googleapis 는 exports map 이 없어 서브패스 import 가 공식적으로 가능하다.)
|
|
12
|
-
* - `auth` 는 AuthPlus 인스턴스라 `google.auth.OAuth2` 등 기존 사용처가 그대로 동작한다.
|
|
13
25
|
*/
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
/** 서브패스 모듈 캐시 — require 자체도 캐시하지만 Map 조회가 더 싸다. */
|
|
29
|
+
const cache = new Map();
|
|
30
|
+
/**
|
|
31
|
+
* googleapis 서브패스를 첫 호출 시점에 동기 로드한다.
|
|
32
|
+
* 첫 호출이 공통 런타임까지 함께 지불하고(~1.1초), 이후 다른 서브패스는 ~20ms 다.
|
|
33
|
+
*/
|
|
34
|
+
function sub(name) {
|
|
35
|
+
let mod = cache.get(name);
|
|
36
|
+
if (mod === undefined) {
|
|
37
|
+
mod = require(`googleapis/build/src/apis/${name}/index.js`);
|
|
38
|
+
cache.set(name, mod);
|
|
39
|
+
}
|
|
40
|
+
return mod;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 기존 `google.<api>(...)` 호출부와 100% 동일하게 동작하는 지연 로딩 네임스페이스.
|
|
44
|
+
* 각 프로퍼티는 getter 라서, 실제로 그 API 를 쓰는 도구가 호출될 때까지 아무것도 로드하지 않는다.
|
|
45
|
+
*/
|
|
27
46
|
export const google = {
|
|
28
|
-
auth
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
// auth 는 AuthPlus 인스턴스. 어느 서브패스든 같은 걸 내보내지만,
|
|
48
|
+
// 기존 동작과 동일하게 firebase 서브패스에서 가져온다.
|
|
49
|
+
get auth() {
|
|
50
|
+
return sub('firebase').auth;
|
|
51
|
+
},
|
|
52
|
+
get admob() {
|
|
53
|
+
return sub('admob').admob;
|
|
54
|
+
},
|
|
55
|
+
get analyticsadmin() {
|
|
56
|
+
return sub('analyticsadmin').analyticsadmin;
|
|
57
|
+
},
|
|
58
|
+
get analyticsdata() {
|
|
59
|
+
return sub('analyticsdata').analyticsdata;
|
|
60
|
+
},
|
|
61
|
+
get androidpublisher() {
|
|
62
|
+
return sub('androidpublisher').androidpublisher;
|
|
63
|
+
},
|
|
64
|
+
get bigquery() {
|
|
65
|
+
return sub('bigquery').bigquery;
|
|
66
|
+
},
|
|
67
|
+
get billingbudgets() {
|
|
68
|
+
return sub('billingbudgets').billingbudgets;
|
|
69
|
+
},
|
|
70
|
+
get cloudbilling() {
|
|
71
|
+
return sub('cloudbilling').cloudbilling;
|
|
72
|
+
},
|
|
73
|
+
get cloudresourcemanager() {
|
|
74
|
+
return sub('cloudresourcemanager').cloudresourcemanager;
|
|
75
|
+
},
|
|
76
|
+
get firebase() {
|
|
77
|
+
return sub('firebase').firebase;
|
|
78
|
+
},
|
|
79
|
+
get iam() {
|
|
80
|
+
return sub('iam').iam;
|
|
81
|
+
},
|
|
82
|
+
get searchconsole() {
|
|
83
|
+
return sub('searchconsole').searchconsole;
|
|
84
|
+
},
|
|
85
|
+
get serviceusage() {
|
|
86
|
+
return sub('serviceusage').serviceusage;
|
|
87
|
+
},
|
|
88
|
+
get youtube() {
|
|
89
|
+
return sub('youtube').youtube;
|
|
90
|
+
},
|
|
42
91
|
};
|
|
@@ -11,7 +11,7 @@ import fs from 'node:fs';
|
|
|
11
11
|
import os from 'node:os';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import zlib from 'node:zlib';
|
|
14
|
-
import {
|
|
14
|
+
import { newJWT } from '../lib/google-auth-lite.js';
|
|
15
15
|
import { fetchWithTimeout, HTTP_TRANSFER_TIMEOUT_MS } from '../lib/http.js';
|
|
16
16
|
import { requireServiceAccountJson } from '../helpers.js';
|
|
17
17
|
const CONFIG_PATH = path.join(os.homedir(), '.mimi-seed', 'play-financials.json');
|
|
@@ -68,7 +68,7 @@ function normalizeBucket(raw) {
|
|
|
68
68
|
*/
|
|
69
69
|
function storageClient(packageName) {
|
|
70
70
|
const parsed = JSON.parse(requireServiceAccountJson(packageName));
|
|
71
|
-
return
|
|
71
|
+
return newJWT({
|
|
72
72
|
email: parsed.client_email,
|
|
73
73
|
key: parsed.private_key,
|
|
74
74
|
scopes: [STORAGE_SCOPE],
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { OAuth2Client } from 'google-auth-library';
|
|
2
|
-
import { JWT } from 'google-auth-library';
|
|
1
|
+
import type { OAuth2Client, JWT } from 'google-auth-library';
|
|
3
2
|
export type PlayImageType = 'featureGraphic' | 'icon' | 'phoneScreenshots' | 'promoGraphic' | 'sevenInchScreenshots' | 'tenInchScreenshots' | 'tvBanner' | 'tvScreenshots' | 'wearScreenshots';
|
|
4
3
|
/**
|
|
5
4
|
* Google Play Developer API (Android Publisher API v3) 래퍼
|
package/dist/playstore/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { google } from '../lib/googleapis-lite.js';
|
|
2
|
-
import {
|
|
2
|
+
import { newJWT } from '../lib/google-auth-lite.js';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { extractHttpStatus } from '../lib/google-errors.js';
|
|
5
5
|
function mimeTypeFor(filePath) {
|
|
@@ -763,7 +763,7 @@ export async function verifyServiceAccountJson(serviceAccountJson, packageName)
|
|
|
763
763
|
message: `Expected type="service_account", got "${parsed.type}" — make sure you downloaded a service account key, not an OAuth client.`,
|
|
764
764
|
};
|
|
765
765
|
}
|
|
766
|
-
const jwt =
|
|
766
|
+
const jwt = newJWT({
|
|
767
767
|
email: parsed.client_email,
|
|
768
768
|
key: parsed.private_key,
|
|
769
769
|
scopes: ['https://www.googleapis.com/auth/androidpublisher'],
|
package/dist/registers/video.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { addLocalAsset, buildTimeline, loadProject, loadTimeline, planStory, } from '../video/project.js';
|
|
2
|
+
import { addLocalAsset, buildTimeline, loadProject, loadTimeline, planStory, savePlan, } from '../video/project.js';
|
|
3
3
|
import { downloadStockAssets, generateImage, researchYouTube, searchStock, } from '../video/providers.js';
|
|
4
4
|
import { getRenderJob, startRender, validateVideo } from '../video/render.js';
|
|
5
5
|
import { synthesizeResearch } from '../video/research.js';
|
|
@@ -67,6 +67,29 @@ export function registerVideoTools(server) {
|
|
|
67
67
|
style: z.string().max(10_000).optional().describe('시각 스타일과 톤'),
|
|
68
68
|
overwrite: z.boolean().default(false).describe('기존 메타데이터를 .history에 보관하고 새 프로젝트로 덮어쓸지 여부'),
|
|
69
69
|
}, async (input) => text(await planStory(input)));
|
|
70
|
+
server.tool('video_save_plan', [
|
|
71
|
+
'에이전트가 직접 작성한 장면별 스토리보드를 project.json으로 저장합니다. API 키가 필요 없는 video_plan_from_story의 무료 대안입니다.',
|
|
72
|
+
'스토리보드(장면 분할·내레이션·화면 문구·시각 프롬프트)는 호출 전에 에이전트가 직접 설계하세요.',
|
|
73
|
+
'장면 durationSec 합계는 targetDurationSec와 일치해야 합니다(허용 오차 0.1초).',
|
|
74
|
+
'기존 project.json은 overwrite=true 없이는 덮어쓰지 않습니다.',
|
|
75
|
+
].join(' '), {
|
|
76
|
+
projectDir: absolutePath,
|
|
77
|
+
title: z.string().min(1).max(500).describe('영상 제목'),
|
|
78
|
+
story: z.string().min(1).max(200_000).describe('영상으로 만들 story 원문'),
|
|
79
|
+
language: z.string().default('ko').describe('내레이션과 화면 문구 언어'),
|
|
80
|
+
aspectRatio: z.enum(['9:16', '16:9', '1:1']).default('9:16').describe('출력 화면비'),
|
|
81
|
+
targetDurationSec: z.number().min(5).max(300).default(30).describe('목표 영상 길이(초)'),
|
|
82
|
+
scenes: z.array(z.object({
|
|
83
|
+
id: z.string().min(1).max(100).optional(),
|
|
84
|
+
durationSec: z.number().positive().max(300),
|
|
85
|
+
narration: z.string().max(10_000).optional(),
|
|
86
|
+
onScreenText: z.string().max(2_000).optional().describe('화면 자막. **단어** 마크업으로 키워드 하이라이트'),
|
|
87
|
+
visualPrompt: z.string().max(10_000).optional().describe('이미지 생성용 프롬프트'),
|
|
88
|
+
searchQuery: z.string().max(1_000).optional().describe('스톡 영상 검색용 영문 검색어'),
|
|
89
|
+
})).min(1).max(12),
|
|
90
|
+
style: z.string().max(10_000).optional().describe('시각 스타일과 톤'),
|
|
91
|
+
overwrite: z.boolean().default(false).describe('기존 메타데이터를 .history에 보관하고 새 프로젝트로 덮어쓸지 여부'),
|
|
92
|
+
}, async (input) => text(savePlan(input)));
|
|
70
93
|
server.tool('video_research_youtube', [
|
|
71
94
|
'스토리보드 검색어나 지정 검색어로 YouTube 유사 영상을 조사합니다.',
|
|
72
95
|
'YOUTUBE_API_KEY가 필요하며 결과는 research/youtube.json에 저장됩니다.',
|
|
@@ -157,21 +180,31 @@ export function registerVideoTools(server) {
|
|
|
157
180
|
server.tool('video_build_timeline', [
|
|
158
181
|
'등록된 이미지·영상 자산을 장면 순서와 길이에 맞춰 timeline.json으로 조합합니다.',
|
|
159
182
|
'assets.json에서 allowedForRendering=true이고 reference-only가 아닌 자산만 허용합니다.',
|
|
160
|
-
'화면 문구는 최종 렌더에서
|
|
183
|
+
'화면 문구는 최종 렌더에서 쇼츠 스타일 자막(굵은 폰트·흰 글자·검정 외곽선)으로 번인되며, onScreenText 안의 **단어** 마크업은 하이라이트 컬러(기본 노랑)로 강조됩니다.',
|
|
184
|
+
'한 장면 자막은 3~5어절·최대 2줄로 짧게 쓰고, 하이라이트는 문장당 한 단어만 지정하세요.',
|
|
161
185
|
].join(' '), {
|
|
162
186
|
projectDir: absolutePath,
|
|
163
187
|
scenes: z.array(z.object({
|
|
164
188
|
id: z.string().min(1),
|
|
165
189
|
assetId: z.string().min(1),
|
|
166
190
|
durationSec: z.number().positive().max(300),
|
|
167
|
-
onScreenText: z.string().optional(),
|
|
191
|
+
onScreenText: z.string().optional().describe('번인 자막. **단어**로 키워드 1개 하이라이트, \\n으로 의미 단위 줄바꿈'),
|
|
168
192
|
narration: z.string().optional(),
|
|
169
193
|
})).min(1).max(30),
|
|
170
194
|
audioAssetId: z.string().optional(),
|
|
171
|
-
|
|
195
|
+
captionStyle: z.object({
|
|
196
|
+
preset: z.enum(['shorts', 'box']).optional().describe('shorts(기본): 외곽선 자막+하이라이트 / box: 반투명 박스 자막(튜토리얼용)'),
|
|
197
|
+
fontName: z.string().min(1).max(200).optional().describe('설치된 굵은 한글 고딕 폰트명(예: Pretendard, 에스코어드림). 생략 시 Malgun Gothic Bold'),
|
|
198
|
+
fontSizePx: z.number().int().min(24).max(400).optional().describe('생략 시 세로 영상은 높이의 4.2%(1080×1920 기준 81px)'),
|
|
199
|
+
textColor: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
|
|
200
|
+
outlineColor: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
|
|
201
|
+
highlightColor: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe('**단어** 마크업 색. 기본 #FFD400'),
|
|
202
|
+
position: z.enum(['lower', 'lower-middle', 'center']).optional().describe('생략 시 9:16은 lower-middle(하단-중앙 1/3, 쇼츠 UI 안전영역)'),
|
|
203
|
+
}).optional().describe('자막 스타일. 생략하면 쇼츠 기본 스타일'),
|
|
204
|
+
}, async ({ projectDir, scenes, audioAssetId, captionStyle }) => text(buildTimeline(projectDir, scenes, audioAssetId, captionStyle)));
|
|
172
205
|
server.tool('video_render', [
|
|
173
206
|
'timeline.json과 등록 자산을 FFmpeg로 합성해 MP4 렌더 작업을 시작합니다.',
|
|
174
|
-
'이미지/영상 크롭, 장면 연결,
|
|
207
|
+
'이미지/영상 크롭, 장면 연결, 쇼츠 스타일 자막 번인(timeline.json의 captionStyle 적용), 선택적 배경음, H.264/yuv420p 출력을 지원합니다.',
|
|
175
208
|
'confirm=false는 계획만 반환합니다. CPU를 사용하는 로컬 쓰기 작업이므로 승인 후 confirm=true로 호출하세요.',
|
|
176
209
|
'즉시 jobId를 반환하므로 video_job_status로 완료 여부를 확인하세요.',
|
|
177
210
|
].join(' '), {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { VideoCaptionStyle, VideoTimeline } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 쇼츠 자막 스타일 시스템 — SRT + force_style(Arial 18px) 하드코딩을 대체한다.
|
|
4
|
+
*
|
|
5
|
+
* 기본 preset 'shorts'는 2026 숏폼 표준(Bold Highlight 스타일)을 따른다:
|
|
6
|
+
* 굵은 고딕 + 흰 글자 + 검정 외곽선(불투명 박스 없음), 9:16에서는 하단-중앙 1/3 배치,
|
|
7
|
+
* `**단어**` 마크업으로 문장당 키워드 1개만 하이라이트 컬러.
|
|
8
|
+
* 'box' preset은 튜토리얼용 반투명 박스 자막(Subtitle Block 스타일)이다.
|
|
9
|
+
*/
|
|
10
|
+
export type CaptionPosition = 'lower' | 'lower-middle' | 'center';
|
|
11
|
+
export interface ResolvedCaptionStyle {
|
|
12
|
+
fontName: string;
|
|
13
|
+
fontSizePx: number;
|
|
14
|
+
textColor: string;
|
|
15
|
+
outlineColor: string;
|
|
16
|
+
highlightColor: string;
|
|
17
|
+
position: CaptionPosition;
|
|
18
|
+
boxed: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function resolveCaptionStyle(width: number, height: number, style?: VideoCaptionStyle): ResolvedCaptionStyle;
|
|
21
|
+
/** #RRGGBB → ASS &HAABBGGRR. alpha 00=불투명, FF=투명. */
|
|
22
|
+
export declare function assColor(hex: string, alpha?: string): string;
|
|
23
|
+
/** ASS 태그·마크업 주입을 막는다. `**`는 하이라이트 마크업으로 살려둔다. */
|
|
24
|
+
declare function sanitizeCaptionText(value: string): string;
|
|
25
|
+
declare function assTime(seconds: number): string;
|
|
26
|
+
/** `**단어**` → 하이라이트 컬러 런. 줄바꿈은 \N. */
|
|
27
|
+
declare function renderEventText(raw: string, style: ResolvedCaptionStyle): string;
|
|
28
|
+
export declare function formatAss(timeline: VideoTimeline, style: ResolvedCaptionStyle): string;
|
|
29
|
+
export declare const __testing: {
|
|
30
|
+
sanitizeCaptionText: typeof sanitizeCaptionText;
|
|
31
|
+
assTime: typeof assTime;
|
|
32
|
+
renderEventText: typeof renderEventText;
|
|
33
|
+
};
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export function resolveCaptionStyle(width, height, style) {
|
|
2
|
+
const preset = style?.preset ?? 'shorts';
|
|
3
|
+
const vertical = height > width;
|
|
4
|
+
return {
|
|
5
|
+
// Malgun Gothic은 어떤 Windows에도 있는 폴백. 스킬은 설치된 굵은 고딕(Pretendard 등)을 우선 지정한다.
|
|
6
|
+
fontName: style?.fontName ?? 'Malgun Gothic',
|
|
7
|
+
// 세로 쇼츠 기준 height의 4.2% ≈ 81px(1080×1920) — 숏폼 권장(55~75pt)과 한국어 예능 자막(72~96px) 사이.
|
|
8
|
+
fontSizePx: style?.fontSizePx ?? Math.round(height * (vertical ? 0.042 : 0.055)),
|
|
9
|
+
textColor: style?.textColor ?? '#FFFFFF',
|
|
10
|
+
outlineColor: style?.outlineColor ?? '#000000',
|
|
11
|
+
highlightColor: style?.highlightColor ?? '#FFD400',
|
|
12
|
+
position: style?.position ?? (preset === 'shorts' && vertical ? 'lower-middle' : 'lower'),
|
|
13
|
+
boxed: preset === 'box',
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** #RRGGBB → ASS &HAABBGGRR. alpha 00=불투명, FF=투명. */
|
|
17
|
+
export function assColor(hex, alpha = '00') {
|
|
18
|
+
const match = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec(hex);
|
|
19
|
+
if (!match)
|
|
20
|
+
throw new Error(`잘못된 색상 형식입니다(#RRGGBB): ${hex}`);
|
|
21
|
+
const [, r, g, b] = match;
|
|
22
|
+
return `&H${alpha}${b}${g}${r}`.toUpperCase();
|
|
23
|
+
}
|
|
24
|
+
/** ASS 태그·마크업 주입을 막는다. `**`는 하이라이트 마크업으로 살려둔다. */
|
|
25
|
+
function sanitizeCaptionText(value) {
|
|
26
|
+
return value
|
|
27
|
+
.replace(/\r/g, '')
|
|
28
|
+
.replace(/\n{2,}/g, '\n')
|
|
29
|
+
.replace(/\\/g, '\')
|
|
30
|
+
.replace(/</g, '<')
|
|
31
|
+
.replace(/>/g, '>')
|
|
32
|
+
.replace(/{/g, '{')
|
|
33
|
+
.replace(/}/g, '}')
|
|
34
|
+
.trim();
|
|
35
|
+
}
|
|
36
|
+
function assTime(seconds) {
|
|
37
|
+
const cs = Math.max(0, Math.round(seconds * 100));
|
|
38
|
+
const hours = Math.floor(cs / 360_000);
|
|
39
|
+
const minutes = Math.floor((cs % 360_000) / 6_000);
|
|
40
|
+
const secs = Math.floor((cs % 6_000) / 100);
|
|
41
|
+
const centis = cs % 100;
|
|
42
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
43
|
+
return `${hours}:${pad(minutes)}:${pad(secs)}.${pad(centis)}`;
|
|
44
|
+
}
|
|
45
|
+
/** `**단어**` → 하이라이트 컬러 런. 줄바꿈은 \N. */
|
|
46
|
+
function renderEventText(raw, style) {
|
|
47
|
+
const primary = assColor(style.textColor);
|
|
48
|
+
const highlight = assColor(style.highlightColor);
|
|
49
|
+
return sanitizeCaptionText(raw)
|
|
50
|
+
.replace(/\*\*([^*\n]+)\*\*/g, `{\\1c${highlight}&}$1{\\1c${primary}&}`)
|
|
51
|
+
.replace(/\n/g, '\\N');
|
|
52
|
+
}
|
|
53
|
+
export function formatAss(timeline, style) {
|
|
54
|
+
const { width, height } = timeline;
|
|
55
|
+
const vertical = height > width;
|
|
56
|
+
const alignment = style.position === 'center' ? 5 : 2;
|
|
57
|
+
const marginX = Math.round(width * 0.09);
|
|
58
|
+
const marginV = style.position === 'center'
|
|
59
|
+
? 0
|
|
60
|
+
: style.position === 'lower-middle'
|
|
61
|
+
? Math.round(height * 0.28)
|
|
62
|
+
: Math.round(height * (vertical ? 0.12 : 0.08));
|
|
63
|
+
// 한글은 받침 내부 공간이 좁아 외곽선을 폰트 크기의 5.5%로 제한한다. box는 outline 값이 패딩이 된다.
|
|
64
|
+
const outlinePx = style.boxed
|
|
65
|
+
? Math.round(style.fontSizePx * 0.25)
|
|
66
|
+
: Math.max(2, Math.round(style.fontSizePx * 0.055));
|
|
67
|
+
const outlineColour = style.boxed ? assColor(style.outlineColor, '50') : assColor(style.outlineColor);
|
|
68
|
+
const events = [];
|
|
69
|
+
let cursor = 0;
|
|
70
|
+
for (const scene of timeline.scenes) {
|
|
71
|
+
const start = cursor;
|
|
72
|
+
cursor += scene.durationSec;
|
|
73
|
+
const text = renderEventText(scene.onScreenText ?? '', style);
|
|
74
|
+
if (!text)
|
|
75
|
+
continue;
|
|
76
|
+
events.push(`Dialogue: 0,${assTime(start)},${assTime(cursor)},Caption,,0,0,0,{\\fad(120,80)}${text}`);
|
|
77
|
+
}
|
|
78
|
+
if (events.length === 0)
|
|
79
|
+
return '';
|
|
80
|
+
return [
|
|
81
|
+
'[Script Info]',
|
|
82
|
+
'ScriptType: v4.00+',
|
|
83
|
+
`PlayResX: ${width}`,
|
|
84
|
+
`PlayResY: ${height}`,
|
|
85
|
+
'WrapStyle: 0',
|
|
86
|
+
'ScaledBorderAndShadow: yes',
|
|
87
|
+
'',
|
|
88
|
+
'[V4+ Styles]',
|
|
89
|
+
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding',
|
|
90
|
+
`Style: Caption,${style.fontName},${style.fontSizePx},${assColor(style.textColor)},${assColor(style.textColor)},${outlineColour},&H80000000,-1,0,0,0,100,100,0,0,${style.boxed ? 3 : 1},${outlinePx},0,${alignment},${marginX},${marginX},${marginV},1`,
|
|
91
|
+
'',
|
|
92
|
+
'[Events]',
|
|
93
|
+
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Text',
|
|
94
|
+
...events,
|
|
95
|
+
'',
|
|
96
|
+
].join('\n');
|
|
97
|
+
}
|
|
98
|
+
export const __testing = { sanitizeCaptionText, assTime, renderEventText };
|
package/dist/video/project.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { VideoAspectRatio, VideoAsset, VideoAssetKind, VideoAssetManifest, VideoAssetSource, VideoProject, VideoScenePlan, VideoTimeline, VideoTimelineScene } from './types.js';
|
|
1
|
+
import type { VideoAspectRatio, VideoAsset, VideoAssetKind, VideoAssetManifest, VideoAssetSource, VideoCaptionStyle, VideoProject, VideoScenePlan, VideoTimeline, VideoTimelineScene } from './types.js';
|
|
2
2
|
declare function dimensions(aspectRatio: VideoAspectRatio): {
|
|
3
3
|
width: number;
|
|
4
4
|
height: number;
|
|
@@ -15,6 +15,23 @@ export interface PlanStoryInput {
|
|
|
15
15
|
overwrite?: boolean;
|
|
16
16
|
}
|
|
17
17
|
export declare function planStory(input: PlanStoryInput): Promise<VideoProject>;
|
|
18
|
+
export interface SavePlanInput {
|
|
19
|
+
projectDir: string;
|
|
20
|
+
title: string;
|
|
21
|
+
story: string;
|
|
22
|
+
language: string;
|
|
23
|
+
aspectRatio: VideoAspectRatio;
|
|
24
|
+
targetDurationSec: number;
|
|
25
|
+
scenes: Array<Partial<VideoScenePlan> & Pick<VideoScenePlan, 'durationSec'>>;
|
|
26
|
+
style?: string;
|
|
27
|
+
overwrite?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 에이전트(구독 세션)가 직접 작성한 스토리보드를 API 키 없이 project.json으로 저장한다.
|
|
31
|
+
* video_plan_from_story(ANTHROPIC_API_KEY 필요)의 무료 대안.
|
|
32
|
+
* 장면 길이 합계가 targetDurationSec와 일치해야 projectSchema 검증을 통과한다.
|
|
33
|
+
*/
|
|
34
|
+
export declare function savePlan(input: SavePlanInput): VideoProject;
|
|
18
35
|
export declare function loadProject(projectDir: string): VideoProject;
|
|
19
36
|
export declare function loadAssets(projectDir: string): VideoAssetManifest;
|
|
20
37
|
export declare function sha256File(filePath: string): string;
|
|
@@ -29,7 +46,7 @@ export interface AddLocalAssetInput {
|
|
|
29
46
|
attribution?: string;
|
|
30
47
|
}
|
|
31
48
|
export declare function addLocalAsset(input: AddLocalAssetInput): VideoAsset;
|
|
32
|
-
export declare function buildTimeline(projectDir: string, scenes: VideoTimelineScene[], audioAssetId?: string): VideoTimeline;
|
|
49
|
+
export declare function buildTimeline(projectDir: string, scenes: VideoTimelineScene[], audioAssetId?: string, captionStyle?: VideoCaptionStyle): VideoTimeline;
|
|
33
50
|
export declare function loadTimeline(projectDir: string): VideoTimeline;
|
|
34
51
|
export declare const __testing: {
|
|
35
52
|
dimensions: typeof dimensions;
|
package/dist/video/project.js
CHANGED
|
@@ -90,6 +90,11 @@ export async function planStory(input) {
|
|
|
90
90
|
},
|
|
91
91
|
scenes: normalizeScenes(parsed.scenes ?? [], input.targetDurationSec),
|
|
92
92
|
}, 'AI가 생성한 영상 프로젝트');
|
|
93
|
+
persistProject(projectDir, project, input.overwrite === true);
|
|
94
|
+
return project;
|
|
95
|
+
}
|
|
96
|
+
function persistProject(projectDir, project, overwrite) {
|
|
97
|
+
const projectPath = path.join(projectDir, PROJECT_FILE);
|
|
93
98
|
mkdirSync(projectDir, { recursive: true });
|
|
94
99
|
mkdirSync(path.join(projectDir, 'assets', 'stock'), { recursive: true });
|
|
95
100
|
mkdirSync(path.join(projectDir, 'assets', 'generated'), { recursive: true });
|
|
@@ -97,7 +102,7 @@ export async function planStory(input) {
|
|
|
97
102
|
mkdirSync(path.join(projectDir, 'research'), { recursive: true });
|
|
98
103
|
mkdirSync(path.join(projectDir, 'render'), { recursive: true });
|
|
99
104
|
mkdirSync(path.join(projectDir, '.jobs'), { recursive: true });
|
|
100
|
-
if (existsSync(projectPath) &&
|
|
105
|
+
if (existsSync(projectPath) && overwrite) {
|
|
101
106
|
const archiveDir = path.join(projectDir, '.history', `${Date.now()}-${project.projectId}`);
|
|
102
107
|
mkdirSync(archiveDir, { recursive: true });
|
|
103
108
|
for (const name of [PROJECT_FILE, ASSET_FILE, TIMELINE_FILE]) {
|
|
@@ -115,6 +120,43 @@ export async function planStory(input) {
|
|
|
115
120
|
projectId: project.projectId,
|
|
116
121
|
assets: [],
|
|
117
122
|
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 에이전트(구독 세션)가 직접 작성한 스토리보드를 API 키 없이 project.json으로 저장한다.
|
|
126
|
+
* video_plan_from_story(ANTHROPIC_API_KEY 필요)의 무료 대안.
|
|
127
|
+
* 장면 길이 합계가 targetDurationSec와 일치해야 projectSchema 검증을 통과한다.
|
|
128
|
+
*/
|
|
129
|
+
export function savePlan(input) {
|
|
130
|
+
const projectDir = requireAbsolutePath(input.projectDir, 'projectDir');
|
|
131
|
+
const projectPath = path.join(projectDir, PROJECT_FILE);
|
|
132
|
+
if (existsSync(projectPath) && !input.overwrite) {
|
|
133
|
+
throw new Error(`이미 영상 프로젝트가 있습니다: ${projectPath}\noverwrite=true로 명시해야 덮어씁니다.`);
|
|
134
|
+
}
|
|
135
|
+
const dims = dimensions(input.aspectRatio);
|
|
136
|
+
const project = parseFile(projectSchema, {
|
|
137
|
+
version: 1,
|
|
138
|
+
projectId: randomUUID(),
|
|
139
|
+
title: input.title,
|
|
140
|
+
story: input.story,
|
|
141
|
+
language: input.language,
|
|
142
|
+
createdAt: new Date().toISOString(),
|
|
143
|
+
settings: {
|
|
144
|
+
aspectRatio: input.aspectRatio,
|
|
145
|
+
...dims,
|
|
146
|
+
fps: 30,
|
|
147
|
+
targetDurationSec: input.targetDurationSec,
|
|
148
|
+
style: input.style,
|
|
149
|
+
},
|
|
150
|
+
scenes: input.scenes.map((scene, index) => ({
|
|
151
|
+
id: scene.id?.trim() || `scene-${index + 1}`,
|
|
152
|
+
durationSec: scene.durationSec,
|
|
153
|
+
narration: scene.narration?.trim() ?? '',
|
|
154
|
+
onScreenText: scene.onScreenText?.trim() ?? '',
|
|
155
|
+
visualPrompt: scene.visualPrompt?.trim() ?? '',
|
|
156
|
+
searchQuery: scene.searchQuery?.trim() || scene.visualPrompt?.trim() || '',
|
|
157
|
+
})),
|
|
158
|
+
}, '에이전트가 작성한 영상 프로젝트');
|
|
159
|
+
persistProject(projectDir, project, input.overwrite === true);
|
|
118
160
|
return project;
|
|
119
161
|
}
|
|
120
162
|
export function loadProject(projectDir) {
|
|
@@ -191,7 +233,7 @@ export function addLocalAsset(input) {
|
|
|
191
233
|
allowedForRendering: true,
|
|
192
234
|
});
|
|
193
235
|
}
|
|
194
|
-
export function buildTimeline(projectDir, scenes, audioAssetId) {
|
|
236
|
+
export function buildTimeline(projectDir, scenes, audioAssetId, captionStyle) {
|
|
195
237
|
const dir = requireAbsolutePath(projectDir, 'projectDir');
|
|
196
238
|
const project = loadProject(dir);
|
|
197
239
|
const manifest = loadAssets(dir);
|
|
@@ -231,6 +273,7 @@ export function buildTimeline(projectDir, scenes, audioAssetId) {
|
|
|
231
273
|
totalDurationSec,
|
|
232
274
|
scenes,
|
|
233
275
|
audioAssetId,
|
|
276
|
+
captionStyle,
|
|
234
277
|
}, '생성할 영상 타임라인');
|
|
235
278
|
writeJsonAtomic(path.join(dir, TIMELINE_FILE), timeline);
|
|
236
279
|
return timeline;
|
package/dist/video/render.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import type { VideoRenderJob
|
|
1
|
+
import type { VideoRenderJob } from './types.js';
|
|
2
2
|
declare function processAlive(pid: number | undefined): boolean;
|
|
3
3
|
declare function readTail(filePath: string, maxBytes?: number): string;
|
|
4
|
-
declare function sanitizeSubtitleText(value: string): string;
|
|
5
|
-
declare function srtTime(seconds: number): string;
|
|
6
|
-
export declare function formatSrt(timeline: VideoTimeline): string;
|
|
7
4
|
export interface FfmpegPlan {
|
|
8
5
|
args: string[];
|
|
9
6
|
outputPath: string;
|
|
@@ -29,9 +26,7 @@ export declare function validateVideo(filePath: string, ffmpegPath?: string): Pr
|
|
|
29
26
|
issues: string[];
|
|
30
27
|
}>;
|
|
31
28
|
export declare const __testing: {
|
|
32
|
-
srtTime: typeof srtTime;
|
|
33
29
|
ffprobeFor: typeof ffprobeFor;
|
|
34
|
-
sanitizeSubtitleText: typeof sanitizeSubtitleText;
|
|
35
30
|
processAlive: typeof processAlive;
|
|
36
31
|
acquireOutputLock: typeof acquireOutputLock;
|
|
37
32
|
readTail: typeof readTail;
|
package/dist/video/render.js
CHANGED
|
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { closeSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
+
import { formatAss, resolveCaptionStyle } from './captions.js';
|
|
6
7
|
import { readJson, writeJsonAtomic } from './files.js';
|
|
7
8
|
import { loadAssets, loadProject, loadTimeline } from './project.js';
|
|
8
9
|
import { parseFile, renderJobSchema } from './schemas.js';
|
|
@@ -41,38 +42,6 @@ function readTail(filePath, maxBytes = 64 * 1024) {
|
|
|
41
42
|
}
|
|
42
43
|
return buffer.toString('utf8');
|
|
43
44
|
}
|
|
44
|
-
function sanitizeSubtitleText(value) {
|
|
45
|
-
return value
|
|
46
|
-
.replace(/\r/g, '')
|
|
47
|
-
.replace(/\n{2,}/g, '\n')
|
|
48
|
-
.replace(/-->/g, '→')
|
|
49
|
-
.replace(/</g, '<')
|
|
50
|
-
.replace(/>/g, '>')
|
|
51
|
-
.replace(/{/g, '{')
|
|
52
|
-
.replace(/}/g, '}')
|
|
53
|
-
.trim();
|
|
54
|
-
}
|
|
55
|
-
function srtTime(seconds) {
|
|
56
|
-
const ms = Math.max(0, Math.round(seconds * 1000));
|
|
57
|
-
const hours = Math.floor(ms / 3_600_000);
|
|
58
|
-
const minutes = Math.floor((ms % 3_600_000) / 60_000);
|
|
59
|
-
const secs = Math.floor((ms % 60_000) / 1000);
|
|
60
|
-
const millis = ms % 1000;
|
|
61
|
-
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')},${String(millis).padStart(3, '0')}`;
|
|
62
|
-
}
|
|
63
|
-
export function formatSrt(timeline) {
|
|
64
|
-
let cursor = 0;
|
|
65
|
-
const blocks = [];
|
|
66
|
-
for (const scene of timeline.scenes) {
|
|
67
|
-
const text = sanitizeSubtitleText(scene.onScreenText ?? '');
|
|
68
|
-
const start = cursor;
|
|
69
|
-
cursor += scene.durationSec;
|
|
70
|
-
if (!text)
|
|
71
|
-
continue;
|
|
72
|
-
blocks.push(`${blocks.length + 1}\n${srtTime(start)} --> ${srtTime(cursor)}\n${text}\n`);
|
|
73
|
-
}
|
|
74
|
-
return blocks.join('\n');
|
|
75
|
-
}
|
|
76
45
|
function isImage(asset) {
|
|
77
46
|
return asset.kind === 'image';
|
|
78
47
|
}
|
|
@@ -111,14 +80,16 @@ export function buildFfmpegPlan(projectDir, outputPath, jobId) {
|
|
|
111
80
|
`crop=${project.settings.width}:${project.settings.height},fps=${project.settings.fps},` +
|
|
112
81
|
`trim=duration=${scene.durationSec},setpts=PTS-STARTPTS,format=yuv420p[v${index}]`));
|
|
113
82
|
filters.push(`${timeline.scenes.map((_, index) => `[v${index}]`).join('')}concat=n=${timeline.scenes.length}:v=1:a=0[base]`);
|
|
114
|
-
const
|
|
83
|
+
const captionStyle = resolveCaptionStyle(project.settings.width, project.settings.height, timeline.captionStyle);
|
|
84
|
+
const ass = formatAss(timeline, captionStyle);
|
|
115
85
|
let videoLabel = '[base]';
|
|
116
86
|
let subtitlePath;
|
|
117
|
-
if (
|
|
118
|
-
const relativeSubtitlePath = `render/captions-${jobId}.
|
|
87
|
+
if (ass) {
|
|
88
|
+
const relativeSubtitlePath = `render/captions-${jobId}.ass`;
|
|
119
89
|
subtitlePath = path.join(dir, relativeSubtitlePath);
|
|
120
|
-
writeFileSync(subtitlePath,
|
|
121
|
-
|
|
90
|
+
writeFileSync(subtitlePath, ass, 'utf8');
|
|
91
|
+
// 스타일(폰트·크기·외곽선·하이라이트)은 ASS 파일에 내장 — force_style을 쓰지 않는다.
|
|
92
|
+
filters.push(`[base]subtitles=${relativeSubtitlePath}[vout]`);
|
|
122
93
|
videoLabel = '[vout]';
|
|
123
94
|
}
|
|
124
95
|
args.push('-filter_complex', filters.join(';'), '-map', videoLabel);
|
|
@@ -317,4 +288,4 @@ export async function validateVideo(filePath, ffmpegPath) {
|
|
|
317
288
|
issues.push(`권장 픽셀 포맷 yuv420p가 아닙니다: ${String(video.pix_fmt)}`);
|
|
318
289
|
return { valid: issues.length === 0, format: data.format ?? {}, streams, issues };
|
|
319
290
|
}
|
|
320
|
-
export const __testing = {
|
|
291
|
+
export const __testing = { ffprobeFor, processAlive, acquireOutputLock, readTail };
|
package/dist/video/schemas.d.ts
CHANGED
|
@@ -350,6 +350,32 @@ export declare const assetManifestSchema: z.ZodObject<{
|
|
|
350
350
|
attribution?: string | undefined;
|
|
351
351
|
}[];
|
|
352
352
|
}>;
|
|
353
|
+
export declare const hexColorSchema: z.ZodString;
|
|
354
|
+
export declare const captionStyleSchema: z.ZodObject<{
|
|
355
|
+
preset: z.ZodOptional<z.ZodEnum<["shorts", "box"]>>;
|
|
356
|
+
fontName: z.ZodOptional<z.ZodString>;
|
|
357
|
+
fontSizePx: z.ZodOptional<z.ZodNumber>;
|
|
358
|
+
textColor: z.ZodOptional<z.ZodString>;
|
|
359
|
+
outlineColor: z.ZodOptional<z.ZodString>;
|
|
360
|
+
highlightColor: z.ZodOptional<z.ZodString>;
|
|
361
|
+
position: z.ZodOptional<z.ZodEnum<["lower", "lower-middle", "center"]>>;
|
|
362
|
+
}, "strip", z.ZodTypeAny, {
|
|
363
|
+
preset?: "shorts" | "box" | undefined;
|
|
364
|
+
fontName?: string | undefined;
|
|
365
|
+
fontSizePx?: number | undefined;
|
|
366
|
+
textColor?: string | undefined;
|
|
367
|
+
outlineColor?: string | undefined;
|
|
368
|
+
highlightColor?: string | undefined;
|
|
369
|
+
position?: "lower" | "lower-middle" | "center" | undefined;
|
|
370
|
+
}, {
|
|
371
|
+
preset?: "shorts" | "box" | undefined;
|
|
372
|
+
fontName?: string | undefined;
|
|
373
|
+
fontSizePx?: number | undefined;
|
|
374
|
+
textColor?: string | undefined;
|
|
375
|
+
outlineColor?: string | undefined;
|
|
376
|
+
highlightColor?: string | undefined;
|
|
377
|
+
position?: "lower" | "lower-middle" | "center" | undefined;
|
|
378
|
+
}>;
|
|
353
379
|
export declare const timelineSceneSchema: z.ZodObject<{
|
|
354
380
|
id: z.ZodString;
|
|
355
381
|
assetId: z.ZodString;
|
|
@@ -397,6 +423,31 @@ export declare const timelineSchema: z.ZodEffects<z.ZodObject<{
|
|
|
397
423
|
onScreenText?: string | undefined;
|
|
398
424
|
}>, "many">;
|
|
399
425
|
audioAssetId: z.ZodOptional<z.ZodString>;
|
|
426
|
+
captionStyle: z.ZodOptional<z.ZodObject<{
|
|
427
|
+
preset: z.ZodOptional<z.ZodEnum<["shorts", "box"]>>;
|
|
428
|
+
fontName: z.ZodOptional<z.ZodString>;
|
|
429
|
+
fontSizePx: z.ZodOptional<z.ZodNumber>;
|
|
430
|
+
textColor: z.ZodOptional<z.ZodString>;
|
|
431
|
+
outlineColor: z.ZodOptional<z.ZodString>;
|
|
432
|
+
highlightColor: z.ZodOptional<z.ZodString>;
|
|
433
|
+
position: z.ZodOptional<z.ZodEnum<["lower", "lower-middle", "center"]>>;
|
|
434
|
+
}, "strip", z.ZodTypeAny, {
|
|
435
|
+
preset?: "shorts" | "box" | undefined;
|
|
436
|
+
fontName?: string | undefined;
|
|
437
|
+
fontSizePx?: number | undefined;
|
|
438
|
+
textColor?: string | undefined;
|
|
439
|
+
outlineColor?: string | undefined;
|
|
440
|
+
highlightColor?: string | undefined;
|
|
441
|
+
position?: "lower" | "lower-middle" | "center" | undefined;
|
|
442
|
+
}, {
|
|
443
|
+
preset?: "shorts" | "box" | undefined;
|
|
444
|
+
fontName?: string | undefined;
|
|
445
|
+
fontSizePx?: number | undefined;
|
|
446
|
+
textColor?: string | undefined;
|
|
447
|
+
outlineColor?: string | undefined;
|
|
448
|
+
highlightColor?: string | undefined;
|
|
449
|
+
position?: "lower" | "lower-middle" | "center" | undefined;
|
|
450
|
+
}>>;
|
|
400
451
|
}, "strip", z.ZodTypeAny, {
|
|
401
452
|
projectId: string;
|
|
402
453
|
version: 1;
|
|
@@ -413,6 +464,15 @@ export declare const timelineSchema: z.ZodEffects<z.ZodObject<{
|
|
|
413
464
|
}[];
|
|
414
465
|
totalDurationSec: number;
|
|
415
466
|
audioAssetId?: string | undefined;
|
|
467
|
+
captionStyle?: {
|
|
468
|
+
preset?: "shorts" | "box" | undefined;
|
|
469
|
+
fontName?: string | undefined;
|
|
470
|
+
fontSizePx?: number | undefined;
|
|
471
|
+
textColor?: string | undefined;
|
|
472
|
+
outlineColor?: string | undefined;
|
|
473
|
+
highlightColor?: string | undefined;
|
|
474
|
+
position?: "lower" | "lower-middle" | "center" | undefined;
|
|
475
|
+
} | undefined;
|
|
416
476
|
}, {
|
|
417
477
|
projectId: string;
|
|
418
478
|
version: 1;
|
|
@@ -429,6 +489,15 @@ export declare const timelineSchema: z.ZodEffects<z.ZodObject<{
|
|
|
429
489
|
}[];
|
|
430
490
|
totalDurationSec: number;
|
|
431
491
|
audioAssetId?: string | undefined;
|
|
492
|
+
captionStyle?: {
|
|
493
|
+
preset?: "shorts" | "box" | undefined;
|
|
494
|
+
fontName?: string | undefined;
|
|
495
|
+
fontSizePx?: number | undefined;
|
|
496
|
+
textColor?: string | undefined;
|
|
497
|
+
outlineColor?: string | undefined;
|
|
498
|
+
highlightColor?: string | undefined;
|
|
499
|
+
position?: "lower" | "lower-middle" | "center" | undefined;
|
|
500
|
+
} | undefined;
|
|
432
501
|
}>, {
|
|
433
502
|
projectId: string;
|
|
434
503
|
version: 1;
|
|
@@ -445,6 +514,15 @@ export declare const timelineSchema: z.ZodEffects<z.ZodObject<{
|
|
|
445
514
|
}[];
|
|
446
515
|
totalDurationSec: number;
|
|
447
516
|
audioAssetId?: string | undefined;
|
|
517
|
+
captionStyle?: {
|
|
518
|
+
preset?: "shorts" | "box" | undefined;
|
|
519
|
+
fontName?: string | undefined;
|
|
520
|
+
fontSizePx?: number | undefined;
|
|
521
|
+
textColor?: string | undefined;
|
|
522
|
+
outlineColor?: string | undefined;
|
|
523
|
+
highlightColor?: string | undefined;
|
|
524
|
+
position?: "lower" | "lower-middle" | "center" | undefined;
|
|
525
|
+
} | undefined;
|
|
448
526
|
}, {
|
|
449
527
|
projectId: string;
|
|
450
528
|
version: 1;
|
|
@@ -461,6 +539,15 @@ export declare const timelineSchema: z.ZodEffects<z.ZodObject<{
|
|
|
461
539
|
}[];
|
|
462
540
|
totalDurationSec: number;
|
|
463
541
|
audioAssetId?: string | undefined;
|
|
542
|
+
captionStyle?: {
|
|
543
|
+
preset?: "shorts" | "box" | undefined;
|
|
544
|
+
fontName?: string | undefined;
|
|
545
|
+
fontSizePx?: number | undefined;
|
|
546
|
+
textColor?: string | undefined;
|
|
547
|
+
outlineColor?: string | undefined;
|
|
548
|
+
highlightColor?: string | undefined;
|
|
549
|
+
position?: "lower" | "lower-middle" | "center" | undefined;
|
|
550
|
+
} | undefined;
|
|
464
551
|
}>;
|
|
465
552
|
export declare const renderJobSchema: z.ZodObject<{
|
|
466
553
|
id: z.ZodString;
|
package/dist/video/schemas.js
CHANGED
|
@@ -56,6 +56,16 @@ export const assetManifestSchema = z.object({
|
|
|
56
56
|
projectId: z.string().uuid(),
|
|
57
57
|
assets: z.array(assetSchema).max(10_000),
|
|
58
58
|
});
|
|
59
|
+
export const hexColorSchema = z.string().regex(/^#[0-9a-fA-F]{6}$/, '#RRGGBB 형식이어야 합니다.');
|
|
60
|
+
export const captionStyleSchema = z.object({
|
|
61
|
+
preset: z.enum(['shorts', 'box']).optional(),
|
|
62
|
+
fontName: z.string().min(1).max(200).optional(),
|
|
63
|
+
fontSizePx: z.number().int().min(24).max(400).optional(),
|
|
64
|
+
textColor: hexColorSchema.optional(),
|
|
65
|
+
outlineColor: hexColorSchema.optional(),
|
|
66
|
+
highlightColor: hexColorSchema.optional(),
|
|
67
|
+
position: z.enum(['lower', 'lower-middle', 'center']).optional(),
|
|
68
|
+
});
|
|
59
69
|
export const timelineSceneSchema = z.object({
|
|
60
70
|
id: z.string().min(1).max(100),
|
|
61
71
|
assetId: z.string().min(1).max(200),
|
|
@@ -73,6 +83,7 @@ export const timelineSchema = z.object({
|
|
|
73
83
|
totalDurationSec: z.number().finite().positive().max(300),
|
|
74
84
|
scenes: z.array(timelineSceneSchema).min(1).max(30),
|
|
75
85
|
audioAssetId: z.string().min(1).max(200).optional(),
|
|
86
|
+
captionStyle: captionStyleSchema.optional(),
|
|
76
87
|
}).superRefine((timeline, ctx) => {
|
|
77
88
|
const calculated = timeline.scenes.reduce((sum, scene) => sum + scene.durationSec, 0);
|
|
78
89
|
if (Math.abs(calculated - timeline.totalDurationSec) > 0.01) {
|
package/dist/video/types.d.ts
CHANGED
|
@@ -53,6 +53,15 @@ export interface VideoTimelineScene {
|
|
|
53
53
|
onScreenText?: string;
|
|
54
54
|
narration?: string;
|
|
55
55
|
}
|
|
56
|
+
export interface VideoCaptionStyle {
|
|
57
|
+
preset?: 'shorts' | 'box';
|
|
58
|
+
fontName?: string;
|
|
59
|
+
fontSizePx?: number;
|
|
60
|
+
textColor?: string;
|
|
61
|
+
outlineColor?: string;
|
|
62
|
+
highlightColor?: string;
|
|
63
|
+
position?: 'lower' | 'lower-middle' | 'center';
|
|
64
|
+
}
|
|
56
65
|
export interface VideoTimeline {
|
|
57
66
|
version: 1;
|
|
58
67
|
projectId: string;
|
|
@@ -63,6 +72,7 @@ export interface VideoTimeline {
|
|
|
63
72
|
totalDurationSec: number;
|
|
64
73
|
scenes: VideoTimelineScene[];
|
|
65
74
|
audioAssetId?: string;
|
|
75
|
+
captionStyle?: VideoCaptionStyle;
|
|
66
76
|
}
|
|
67
77
|
export type VideoJobStatus = 'queued' | 'running' | 'completed' | 'failed';
|
|
68
78
|
export interface VideoRenderJob {
|
package/package.json
CHANGED
package/tool-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "등록된 MCP 도구 + 도메인 메타데이터(label·credential·summary)의 SSOT. src/__tests__/tool-manifest.test.ts 가 실제 서버 등록 목록과 diff 하고 메타데이터 정합성을 검사한다. 도구 추가/삭제/개명 시 이 파일을 함께 갱신할 것 — 산문 문서에는 정확한 개수를 쓰지 말고 이 파일을 가리킬 것. mimi-seed://tools/catalog 리소스가 이 파일을 그대로 서빙한다.",
|
|
3
|
-
"total":
|
|
3
|
+
"total": 231,
|
|
4
4
|
"domains": {
|
|
5
5
|
"admob": {
|
|
6
6
|
"label": "AdMob",
|
|
@@ -367,6 +367,7 @@
|
|
|
367
367
|
"youtube_get_video_status",
|
|
368
368
|
"youtube_update_video_privacy",
|
|
369
369
|
"video_plan_from_story",
|
|
370
|
+
"video_save_plan",
|
|
370
371
|
"video_research_youtube",
|
|
371
372
|
"video_search_stock_assets",
|
|
372
373
|
"video_synthesize_research",
|