# 編集

> TypeScript から、ポッドキャストや長い動画を編集します。無音を検出し、複数の録音を同期し、文字起こしからカットをプランし、編集決定リストをレンダリングします。

Source: https://nodaro.ai/ja/docs/developers/sdk/editing

**`client.edit`** には、ポッドキャストや長い動画のための編集ツールがまとまっています。無音を見つけ、複数の録音がどれだけずれているかを測り、文字起こしからカットをプランし、**編集決定リスト**（EDL）を完成した動画ファイルまたは音声ファイルにレンダリングします。4 つのメソッドはジョブを開始して `{ jobId }` を返し、[`client.jobs.getStatus()`](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) でポーリングします。5 つ目の `remapTranscript()` は、リクエストなしでローカルに実行されます。

## メソッド
| メソッド | エンドポイント | 内容 |
| --- | --- | --- |
| [`edit.silenceDetect(input)`](#editsilencedetectinput) | `POST /v1/silence-detect` | オーディオまたは動画のソースから、無音の範囲を見つけます |
| [`edit.audioSync(input)`](#editaudiosyncinput) | `POST /v1/audio-sync` | 2〜6 件の録音の間の時間のずれを測ります |
| [`edit.editPlan(input)`](#editeditplaninput) | `POST /v1/edit-plan` | 文字起こしから、詰めたカット、短いクリップ、チャプターのいずれかをプランします |
| [`edit.applyEdl(input)`](#editapplyedlinput) | `POST /v1/apply-edl` | EDL を動画ファイルまたは音声ファイルにレンダリングします |
| [`edit.remapTranscript(edl, transcript)`](#editremaptranscriptedl-transcript) | なし、ローカル | 文字起こしのタイミングを、編集後のタイムラインに移します |

## コードでポッドキャストを編集する
### 録音を文字起こしする
単語単位のタイミングを返すエンジンを指定して、[`client.audio.transcribe()`](https://nodaro.ai/docs/developers/sdk/voices-and-audio#audiotranscribeinput) を実行します。その `output_data.json` が、プランに必要な文字起こしです。

### 無音を見つける
マスター録音に対して `silenceDetect()` を実行します。その `output_data.json` に、無音の範囲が入ります。

### カットをプランする
文字起こし、ソース、無音の情報を指定して、`editPlan()` を `tighten` モードで実行します。プランは `unwrapEditPlanOutput()` で読み取ります。

### レンダリングする
プランを `applyEdl()` に渡します。完了したジョブに、編集後の動画または音声が入ります。

Workflow: ノードと同じ編集の連鎖です。文字起こしと無音の検出を行い、詰めたカットをプランして、レンダリングします。

- 動画アップロード → 文字起こし
- 動画アップロード → 無音検出
- 文字起こし → 編集プラン
- 無音検出 → 編集プラン
- 編集プラン → EDL 適用

```ts

async function outputOf(jobId: string): Promise<any> {
for (;;) {
const { data } = await client.jobs.getStatus(jobId)
if (data.status === "completed") return data.output_data
if (data.status === "failed" || data.status === "cancelled") throw new Error(data.error_message ?? data.status)
await new Promise((resolve) => setTimeout(resolve, 3_000))
}
}

// 1. Transcribe with word timings
const tr = await client.audio.transcribe({ audioUrl: masterUrl, provider: "elevenlabs-stt" })
const transcript = (await outputOf(tr.jobId)).json

// 2. Find the silence
const sd = await client.edit.silenceDetect({ audioUrl: masterUrl, thresholdDb: -35 })
const silence = (await outputOf(sd.jobId)).json

// 3. Plan a tighter cut
const plan = await client.edit.editPlan({
mode: "tighten",
planTier: "standard",
transcript,
sources: [{ id: "ep", url: masterUrl, kind: "video", role: "master-audio" }],
silence,
})
const edl = unwrapEditPlanOutput(await outputOf(plan.jobId))

// 4. Render the plan
const render = await client.edit.applyEdl({ edl, output: "video", quality: "final" })
const { videoUrl } = await outputOf(render.jobId)
```

同じ手順が、ノードとしても存在します。[**文字起こし**（Transcribe）](https://nodaro.ai/docs/nodes/audio/transcribe)、[**無音検出**（Silence Detect）](https://nodaro.ai/docs/nodes/audio/silence-detect)、[**編集プラン**（Edit Plan）](https://nodaro.ai/docs/nodes/video/edit-plan)、[**EDL 適用**（Apply EDL）](https://nodaro.ai/docs/nodes/video/apply-edl)です。

## client.edit
### edit.silenceDetect(input)
オーディオまたは動画のソースから、無音の範囲を見つけます。サーバー上で、AI モデルなしに実行されます。

```ts
silenceDetect(input: SilenceDetectInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrl: { type: 'string', required: true, description: "オーディオまたは動画のソースです。" },
thresholdDb: { type: 'number', default: '-35', description: "これを下回る音量を無音と見なす、dBFS 単位のしきい値です。0 以下です。" },
minSilenceMs: { type: 'number', default: '700', description: "報告する最短の無音の長さで、ミリ秒単位です。" },
padMs: { type: 'number', default: '120', description: "発話の前後に残す余白で、ミリ秒単位です。各無音の範囲を、その分だけ短くします。" },
workflowId: { type: 'string', description: "この実行を一覧表示する対象のワークフローで、その実行履歴に表示されます。" },
}}
/>

```ts
const { jobId } = await client.edit.silenceDetect({ audioUrl: masterUrl, minSilenceMs: 900 })
```

完了したジョブの `output_data.json` は、`SilenceRanges` オブジェクトです。`{ version, ranges: [{ startMs, endMs }], durationMs }` です。このオブジェクト全体を、`silence` として `editPlan()` に渡します。

### edit.audioSync(input)
1 つの会話を収録した 2〜6 件の録音について、その音から、時計がどれだけずれているかを測ります。[**音声同期**（Audio Sync）](https://nodaro.ai/docs/nodes/audio/audio-sync)ノードと同じ処理です。サーバー上で、AI モデルなしに実行されます。料金は `10 × (sources − 1)` クレジットです。2 件のソースで 10、4 件で 30、6 件で 50 です。

```ts
audioSync(input: AudioSyncInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
sources: { type: 'Array<{ id: string; url: string }>', required: true, description: "2〜6 件の録音です。各 id は 1〜200 文字で一意である必要があり、そのオフセットの sourceId として返ります。同じ録音を指す EDL の id を、そのまま使ってください。" },
reference: { type: 'string', description: "すべてのオフセットの基準にするソースの id です。そのソース自身のオフセットは 0 になります。デフォルトは、最初のソースです。" },
workflowId: { type: 'string', description: "この実行を一覧表示する対象のワークフローです。" },
}}
/>

```ts
const { jobId } = await client.edit.audioSync({
sources: [
{ id: "mic", url: micUrl },
{ id: "camA", url: camAUrl },
],
reference: "mic",
})
```

完了したジョブの `output_data.json` は、`AudioSyncResult` です。

```ts
{
version: number
reference: string // the source every offset is measured against
offsets: Array<{
sourceId: string
offsetMs: number              // referenceMs = sourceMs + offsetMs
confidence: number            // 0 to 1; below 0.5 a note asks you to check by ear
driftMsPerHour: number | null // measured, never corrected; null when the overlap was too short
}>
notes: string[] // low confidence, drift above 33 ms over the overlap, no shared sound
}
```

マスター録音を `reference` にすると、各 `offsetMs` は、あなたの EDL におけるそのソースの `offsetMs` と正確に一致します。不正な形式のリクエストは、クレジットが確保される前に、`code` が `validation_error` の `NodaroError` で拒否されます。これには、ソースが 2 件未満または 6 件を超える場合、id が重複している場合、`reference` がどの id とも一致しない場合が含まれます。

### edit.editPlan(input)
タイミング付きの文字起こしから、編集をプランします。[編集プラン](https://nodaro.ai/docs/nodes/video/edit-plan)ノードと同じ処理です。3 種類のプランのいずれかを作成します。録音全体を詰めたカット、短いクリップのセット、チャプターです。

```ts
editPlan(input: EditPlanInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
mode: { type: '"tighten" | "clips" | "chapters"', required: true, description: "tighten は間やフィラーを取り除き、clips は短いクリップを切り出し、chapters は録音をチャプターに分割します。" },
planTier: { type: '"economy" | "standard" | "premium"', required: true, description: "モデルのティアです。品質と料金を決めます。" },
transcript: { type: 'Transcript', required: true, description: "タイミング付きの文字起こしです。文字起こしジョブの output_data.json です。" },
sources: { type: 'EditPlanSource[]', required: true, description: "1〜6 件のソースです。各ソースは { id, url, kind, role?, speakers?, offsetMs? } で、kind は video または audio です。" },
silence: { type: 'SilenceRanges', description: "silenceDetect ジョブの output_data.json です。オブジェクト全体を渡してください。ranges を持たないものは無視されます。" },
instructions: { type: 'string', description: "自由形式の編集の指示です。" },
styleGuide: { type: 'string', description: "従うべきスタイルガイドです。" },
count: { type: 'number', description: "clips モード：切り出すクリップの数です。" },
targetDurationSec: { type: 'number', description: "clips モード：各クリップの目標の長さです。" },
targetAspect: { type: '"16:9" | "9:16" | "1:1" | "4:5"', description: "クリップのアスペクト比です。" },
platform: { type: 'string', description: "クリップの投稿先のプラットフォームです。" },
workflowId: { type: 'string', description: "この実行を一覧表示する対象のワークフローです。" },
}}
/>

```ts

const { jobId } = await client.edit.editPlan({
mode: "clips",
planTier: "standard",
transcript,
sources: [{ id: "ep", url: masterUrl, kind: "video", role: "master-audio" }],
silence,
count: 5,
targetAspect: "9:16",
})
const { data } = await client.jobs.getStatus(jobId) // poll until completed
const clips = unwrapEditPlanOutput(data.output_data) // one EDL per clip
```

完了したジョブの出力は、**`unwrapEditPlanOutput()`** で読み取ります。`tighten` モードでは `Edl` を、`clips` モードでは `Edl` の配列を、`chapters` モードでは `ChapterSet` を返します。

セルフホスティング環境では、このメソッドに [Nodaro Cloud への接続](https://nodaro.ai/docs/self-hosting/cloud-connect)が必要です。接続がない場合、`503 nodaro_connection_required` で失敗します。

### edit.applyEdl(input)
編集決定リストを、動画ファイルまたは音声ファイルにレンダリングします。[EDL 適用](https://nodaro.ai/docs/nodes/video/apply-edl)ノードと同じ処理です。

```ts
applyEdl(input: ApplyEdlInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
edl: { type: 'Edl', required: true, description: "レンダリングする編集決定リストです。メディアは、それぞれのソースの url から取得されます。" },
sources: { type: 'string[]', description: "EDL のソースを置き換えるメディアの URL で、同じ順番で指定します。" },
transcript: { type: 'Transcript', description: "編集後のタイムラインに移す文字起こしです。結果は、ジョブの json 出力で返ります。" },
output: { type: '"video" | "audio"', default: '"video"', description: "レンダリングする内容です。" },
quality: { type: '"proxy" | "final"', default: '"final"', description: "proxy はすばやいプレビューをレンダリングします。final はフル品質でレンダリングします。" },
crossfadeMs: { type: 'number', default: '0', description: "独自のトランジションを持たないすべてのカットに適用する、クロスフェードの長さで、ミリ秒単位です。0 はハードカットを意味します。" },
workflowId: { type: 'string', description: "この実行を一覧表示する対象のワークフローです。" },
}}
/>

```ts
const { jobId } = await client.edit.applyEdl({ edl, output: "video", quality: "proxy", crossfadeMs: 80 })
```

EDL は、クレジットが確保される前にチェックされます。不明なソース、動画編集で映像のないセグメント、1 回のレンダリングで**出力が 180 分を超える**場合のいずれかは、`code` が `invalid_edl` の `NodaroError` で失敗します。長さは、クロスフェードを適用した後で測られます。`message` には、問題の内容が示されます。たとえば、編集が 200 分になり、最大 180 分の部分に分割する必要がある、といった内容です。

### edit.remapTranscript(edl, transcript)
文字起こしを、**リクエストなしにローカルで**、編集のタイムラインに移します。カットされた範囲内の単語は取り除かれ、カットをまたぐ単語は切り詰められ、すべてのタイミングが編集後の出力に合わせてずらされます。`transcript` を渡した場合、`applyEdl()` はサーバー上で同じ処理を行います。

```ts
remapTranscript(edl: Edl, transcript: Transcript): Transcript
```

<TypeTable
type={{
edl: { type: 'Edl', required: true, description: "編集の内容です。" },
transcript: { type: 'Transcript', required: true, description: "元の録音の文字起こしです。" },
}}
/>

```ts
const editedTranscript = client.edit.remapTranscript(edl, transcript)
// caption the edited video without another transcription
```

レンダリングや 2 回目の文字起こしなしに、編集に字幕を付けるために使います。新しいタイミングだけが必要な場合、大きな文字起こしに対しては、こちらのほうが高速な選択肢でもあります。

## Frequently asked questions

### Nodaro SDK で、ポッドキャストから間を取り除くには、どうすればよいですか？

録音を文字起こしし、client.edit.silenceDetect を実行してから、文字起こしと無音の範囲を指定して client.edit.editPlan を tighten モードで実行します。返されたプランを、client.edit.applyEdl でレンダリングします。

### EDL とは何ですか？

編集決定リストのことです。編集の素材と、残すセグメントを、トランジションとともに順番に並べたものです。client.edit.editPlan がこれを作成し、client.edit.applyEdl が動画ファイルまたは音声ファイルにレンダリングします。

### 1 回の EDL のレンダリングは、どのくらいの長さにできますか？

1 回のレンダリングにつき、出力は最大 180 分です。これより長い編集は、クレジットが確保される前に invalid_edl で拒否されます。いくつかの部分に分けてください。

### client.edit.audioSync の料金は、どのくらいですか？

最初の 1 件を除き、1 件につき 10 クレジットです。2 件の録音で 10 クレジット、4 件で 30 クレジット、6 件で 50 クレジットです。
