davinci-cli 1.0.1__py3-none-any.whl
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.
- davinci_cli/__init__.py +3 -0
- davinci_cli/cli.py +109 -0
- davinci_cli/commands/__init__.py +0 -0
- davinci_cli/commands/beat_markers.py +239 -0
- davinci_cli/commands/clip.py +457 -0
- davinci_cli/commands/color.py +1045 -0
- davinci_cli/commands/deliver.py +647 -0
- davinci_cli/commands/gallery.py +366 -0
- davinci_cli/commands/mcp_cmd.py +183 -0
- davinci_cli/commands/media.py +673 -0
- davinci_cli/commands/project.py +422 -0
- davinci_cli/commands/schema.py +58 -0
- davinci_cli/commands/system.py +229 -0
- davinci_cli/commands/timeline.py +989 -0
- davinci_cli/core/__init__.py +0 -0
- davinci_cli/core/connection.py +79 -0
- davinci_cli/core/edition.py +77 -0
- davinci_cli/core/environment.py +92 -0
- davinci_cli/core/exceptions.py +104 -0
- davinci_cli/core/logging.py +66 -0
- davinci_cli/core/validation.py +118 -0
- davinci_cli/decorators.py +86 -0
- davinci_cli/mcp/__init__.py +0 -0
- davinci_cli/mcp/instructions.py +66 -0
- davinci_cli/mcp/mcp_server.py +1671 -0
- davinci_cli/output/__init__.py +0 -0
- davinci_cli/output/formatter.py +70 -0
- davinci_cli/schema_registry.py +21 -0
- davinci_cli-1.0.1.dist-info/METADATA +29 -0
- davinci_cli-1.0.1.dist-info/RECORD +33 -0
- davinci_cli-1.0.1.dist-info/WHEEL +4 -0
- davinci_cli-1.0.1.dist-info/entry_points.txt +3 -0
- davinci_cli-1.0.1.dist-info/licenses/LICENSE +21 -0
davinci_cli/__init__.py
ADDED
davinci_cli/cli.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""CLI エントリポイント。
|
|
2
|
+
|
|
3
|
+
エントリポイント名は 'dr' に統一。'cli' は使わない。
|
|
4
|
+
グローバルエラーハンドリングは DavinciCLIGroup.invoke() オーバーライドで実装。
|
|
5
|
+
各コマンドに手動でデコレータを付ける必要はない。
|
|
6
|
+
|
|
7
|
+
exit_code は core/exceptions.py の定義を参照する:
|
|
8
|
+
1: ResolveNotRunningError
|
|
9
|
+
2: ProjectNotOpenError / ProjectNotFoundError
|
|
10
|
+
3: ValidationError
|
|
11
|
+
4: DavinciEnvironmentError
|
|
12
|
+
5: EditionError
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
from davinci_cli.core.exceptions import DavinciCLIError
|
|
22
|
+
from davinci_cli.core.logging import setup_logging
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DavinciCLIGroup(click.Group):
|
|
26
|
+
"""カスタム Click Group — invoke オーバーライドでグローバルエラーハンドリング。"""
|
|
27
|
+
|
|
28
|
+
def invoke(self, ctx: click.Context) -> None:
|
|
29
|
+
try:
|
|
30
|
+
super().invoke(ctx)
|
|
31
|
+
except DavinciCLIError as exc:
|
|
32
|
+
error_response = {
|
|
33
|
+
"error": str(exc),
|
|
34
|
+
"error_type": type(exc).__name__,
|
|
35
|
+
"exit_code": exc.exit_code,
|
|
36
|
+
}
|
|
37
|
+
click.echo(json.dumps(error_response, ensure_ascii=False))
|
|
38
|
+
ctx.exit(exc.exit_code)
|
|
39
|
+
except click.exceptions.ClickException:
|
|
40
|
+
raise # Click の UsageError/BadParameter 等はそのまま伝播
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
error_response = {
|
|
43
|
+
"error": str(exc),
|
|
44
|
+
"error_type": type(exc).__name__,
|
|
45
|
+
"exit_code": 1,
|
|
46
|
+
}
|
|
47
|
+
click.echo(json.dumps(error_response, ensure_ascii=False))
|
|
48
|
+
ctx.exit(1)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@click.group(cls=DavinciCLIGroup)
|
|
52
|
+
@click.version_option()
|
|
53
|
+
@click.option(
|
|
54
|
+
"--pretty",
|
|
55
|
+
is_flag=True,
|
|
56
|
+
default=False,
|
|
57
|
+
help="Human-readable output (TTY only)",
|
|
58
|
+
)
|
|
59
|
+
@click.option(
|
|
60
|
+
"--verbose",
|
|
61
|
+
"-v",
|
|
62
|
+
is_flag=True,
|
|
63
|
+
default=False,
|
|
64
|
+
help="Enable verbose logging (INFO level)",
|
|
65
|
+
)
|
|
66
|
+
@click.option(
|
|
67
|
+
"--debug",
|
|
68
|
+
is_flag=True,
|
|
69
|
+
default=False,
|
|
70
|
+
help="Enable debug logging (DEBUG level)",
|
|
71
|
+
)
|
|
72
|
+
@click.pass_context
|
|
73
|
+
def dr(ctx: click.Context, pretty: bool, verbose: bool, debug: bool) -> None:
|
|
74
|
+
"""DaVinci Resolve CLI — agent-first interface."""
|
|
75
|
+
ctx.ensure_object(dict)
|
|
76
|
+
ctx.obj["pretty"] = pretty
|
|
77
|
+
ctx.obj["verbose"] = verbose
|
|
78
|
+
ctx.obj["debug"] = debug
|
|
79
|
+
|
|
80
|
+
setup_logging(verbose=verbose, debug=debug)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _register_commands() -> None:
|
|
84
|
+
from davinci_cli.commands import (
|
|
85
|
+
clip,
|
|
86
|
+
color,
|
|
87
|
+
deliver,
|
|
88
|
+
gallery,
|
|
89
|
+
mcp_cmd,
|
|
90
|
+
media,
|
|
91
|
+
project,
|
|
92
|
+
schema,
|
|
93
|
+
system,
|
|
94
|
+
timeline,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
dr.add_command(system.system)
|
|
98
|
+
dr.add_command(schema.schema)
|
|
99
|
+
dr.add_command(project.project)
|
|
100
|
+
dr.add_command(timeline.timeline)
|
|
101
|
+
dr.add_command(clip.clip)
|
|
102
|
+
dr.add_command(color.color)
|
|
103
|
+
dr.add_command(media.media)
|
|
104
|
+
dr.add_command(deliver.deliver)
|
|
105
|
+
dr.add_command(gallery.gallery)
|
|
106
|
+
dr.add_command(mcp_cmd.mcp)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
_register_commands()
|
|
File without changes
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""dr timeline marker beats — BPMベースのマーカー自動配置。
|
|
2
|
+
|
|
3
|
+
BPM と音価を指定して、指定クリップの範囲に等間隔マーカーを配置する。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from davinci_cli.core.connection import get_resolve
|
|
14
|
+
from davinci_cli.core.exceptions import ProjectNotOpenError, ValidationError
|
|
15
|
+
from davinci_cli.decorators import dry_run_option, json_input_option
|
|
16
|
+
from davinci_cli.output.formatter import output
|
|
17
|
+
from davinci_cli.schema_registry import register_schema
|
|
18
|
+
|
|
19
|
+
# 音価 → 1拍あたりの倍率マッピング
|
|
20
|
+
NOTE_VALUE_MAP: dict[str, float] = {
|
|
21
|
+
"1/1": 4.0, # 全音符 = 4拍
|
|
22
|
+
"1/2": 2.0, # 2分音符 = 2拍
|
|
23
|
+
"1/4": 1.0, # 4分音符 = 1拍
|
|
24
|
+
"1/8": 0.5, # 8分音符 = 0.5拍
|
|
25
|
+
"1/16": 0.25, # 16分音符 = 0.25拍
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _calculate_beat_frames(
|
|
30
|
+
bpm: float,
|
|
31
|
+
note_value: str,
|
|
32
|
+
fps: float,
|
|
33
|
+
start_frame: int,
|
|
34
|
+
end_frame: int,
|
|
35
|
+
) -> list[int]:
|
|
36
|
+
"""BPM・音価・FPSからマーカーを打つべきフレーム一覧を計算する。
|
|
37
|
+
|
|
38
|
+
誤差蓄積防止: 各フレームは start_frame + round(i * interval) で計算。
|
|
39
|
+
累積加算(frame += interval)は使わない。
|
|
40
|
+
"""
|
|
41
|
+
beats_per_note = NOTE_VALUE_MAP[note_value]
|
|
42
|
+
# 1音価あたりの秒数
|
|
43
|
+
seconds_per_beat = (60.0 / bpm) * beats_per_note
|
|
44
|
+
# 1音価あたりのフレーム数(浮動小数点)
|
|
45
|
+
frames_per_beat = seconds_per_beat * fps
|
|
46
|
+
|
|
47
|
+
frames: list[int] = []
|
|
48
|
+
i = 0
|
|
49
|
+
while True:
|
|
50
|
+
frame = start_frame + round(i * frames_per_beat)
|
|
51
|
+
if frame > end_frame:
|
|
52
|
+
break
|
|
53
|
+
frames.append(frame)
|
|
54
|
+
i += 1
|
|
55
|
+
return frames
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# --- Pydantic Models ---
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class BeatMarkerInput(BaseModel):
|
|
62
|
+
bpm: float
|
|
63
|
+
clip_index: int
|
|
64
|
+
note_value: str = "1/4"
|
|
65
|
+
color: str = "Blue"
|
|
66
|
+
name: str = ""
|
|
67
|
+
duration: int = 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BeatMarkerOutput(BaseModel):
|
|
71
|
+
added_count: int | None = None
|
|
72
|
+
requested_count: int | None = None
|
|
73
|
+
failed_frames: list[int] | None = None
|
|
74
|
+
bpm: float | None = None
|
|
75
|
+
note_value: str | None = None
|
|
76
|
+
color: str | None = None
|
|
77
|
+
clip_name: str | None = None
|
|
78
|
+
frames: list[int] | None = None
|
|
79
|
+
dry_run: bool | None = None
|
|
80
|
+
action: str | None = None
|
|
81
|
+
count: int | None = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# --- Helper ---
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _get_current_project() -> Any:
|
|
88
|
+
resolve = get_resolve()
|
|
89
|
+
pm = resolve.GetProjectManager()
|
|
90
|
+
project = pm.GetCurrentProject()
|
|
91
|
+
if project is None:
|
|
92
|
+
raise ProjectNotOpenError()
|
|
93
|
+
return project
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _collect_clips(tl: Any) -> list[tuple[dict[str, Any], Any]]:
|
|
97
|
+
"""タイムラインから全クリップを収集する。"""
|
|
98
|
+
clips: list[tuple[dict[str, Any], Any]] = []
|
|
99
|
+
for track_type in ["video", "audio"]:
|
|
100
|
+
track_count = tl.GetTrackCount(track_type)
|
|
101
|
+
for track_idx in range(1, track_count + 1):
|
|
102
|
+
track_clips = tl.GetItemListInTrack(track_type, track_idx) or []
|
|
103
|
+
for clip_item in track_clips:
|
|
104
|
+
info = {
|
|
105
|
+
"index": len(clips),
|
|
106
|
+
"name": clip_item.GetName(),
|
|
107
|
+
"start": clip_item.GetStart(),
|
|
108
|
+
"end": clip_item.GetEnd(),
|
|
109
|
+
"type": track_type,
|
|
110
|
+
"track": track_idx,
|
|
111
|
+
}
|
|
112
|
+
clips.append((info, clip_item))
|
|
113
|
+
return clips
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# --- _impl Function ---
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def beat_marker_impl(
|
|
120
|
+
bpm: float,
|
|
121
|
+
clip_index: int,
|
|
122
|
+
note_value: str = "1/4",
|
|
123
|
+
color: str = "Blue",
|
|
124
|
+
name: str = "",
|
|
125
|
+
duration: int = 1,
|
|
126
|
+
dry_run: bool = False,
|
|
127
|
+
) -> dict[str, Any]:
|
|
128
|
+
"""BPM と音価を指定して、指定クリップの範囲にマーカーを配置する。"""
|
|
129
|
+
# 1. バリデーション
|
|
130
|
+
if note_value not in NOTE_VALUE_MAP:
|
|
131
|
+
valid = ", ".join(NOTE_VALUE_MAP)
|
|
132
|
+
raise ValidationError(
|
|
133
|
+
field="note_value",
|
|
134
|
+
reason=f"Invalid note_value: '{note_value}'. Must be one of: {valid}",
|
|
135
|
+
)
|
|
136
|
+
if not (20.0 <= bpm <= 300.0):
|
|
137
|
+
raise ValidationError(
|
|
138
|
+
field="bpm",
|
|
139
|
+
reason=f"BPM must be between 20.0 and 300.0, got {bpm}",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# 2. タイムライン情報取得
|
|
143
|
+
project = _get_current_project()
|
|
144
|
+
tl = project.GetCurrentTimeline()
|
|
145
|
+
if not tl:
|
|
146
|
+
raise ProjectNotOpenError()
|
|
147
|
+
fps = float(tl.GetSetting("timelineFrameRate") or "24")
|
|
148
|
+
|
|
149
|
+
# 3. クリップ取得
|
|
150
|
+
clips = _collect_clips(tl)
|
|
151
|
+
if clip_index < 0 or clip_index >= len(clips):
|
|
152
|
+
raise ValidationError(
|
|
153
|
+
field="clip_index",
|
|
154
|
+
reason=f"Clip index {clip_index} out of range (0..{len(clips) - 1})",
|
|
155
|
+
)
|
|
156
|
+
clip_info, _clip_item = clips[clip_index]
|
|
157
|
+
start_frame = clip_info["start"]
|
|
158
|
+
end_frame = clip_info["end"]
|
|
159
|
+
clip_name = clip_info["name"]
|
|
160
|
+
|
|
161
|
+
# 4. フレーム計算(クリップの start〜end 範囲)
|
|
162
|
+
frames = _calculate_beat_frames(bpm, note_value, fps, start_frame, end_frame)
|
|
163
|
+
|
|
164
|
+
# 5. dry-run
|
|
165
|
+
if dry_run:
|
|
166
|
+
return {
|
|
167
|
+
"dry_run": True,
|
|
168
|
+
"action": "marker_beats",
|
|
169
|
+
"bpm": bpm,
|
|
170
|
+
"note_value": note_value,
|
|
171
|
+
"color": color,
|
|
172
|
+
"clip_name": clip_name,
|
|
173
|
+
"count": len(frames),
|
|
174
|
+
"frames": frames,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# 6. マーカー追加(マーカーAPIは相対フレームを要求、空nameは拒否される)
|
|
178
|
+
from davinci_cli.commands.timeline import _get_start_frame_offset
|
|
179
|
+
|
|
180
|
+
offset = _get_start_frame_offset(tl)
|
|
181
|
+
marker_name = name or " "
|
|
182
|
+
added_count = 0
|
|
183
|
+
failed_frames = []
|
|
184
|
+
for frame_abs in frames:
|
|
185
|
+
rel_frame = frame_abs - offset
|
|
186
|
+
if tl.AddMarker(rel_frame, color, marker_name, "", duration):
|
|
187
|
+
added_count += 1
|
|
188
|
+
else:
|
|
189
|
+
failed_frames.append(frame_abs)
|
|
190
|
+
|
|
191
|
+
result = {
|
|
192
|
+
"added_count": added_count,
|
|
193
|
+
"requested_count": len(frames),
|
|
194
|
+
"bpm": bpm,
|
|
195
|
+
"note_value": note_value,
|
|
196
|
+
"color": color,
|
|
197
|
+
"clip_name": clip_name,
|
|
198
|
+
"frames": frames,
|
|
199
|
+
}
|
|
200
|
+
if failed_frames:
|
|
201
|
+
result["failed_frames"] = failed_frames
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# --- CLI Command ---
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@click.command(name="beats")
|
|
209
|
+
@json_input_option
|
|
210
|
+
@dry_run_option
|
|
211
|
+
@click.pass_context
|
|
212
|
+
def beat_marker_cmd(
|
|
213
|
+
ctx: click.Context,
|
|
214
|
+
json_input: dict[str, Any] | None,
|
|
215
|
+
dry_run: bool,
|
|
216
|
+
) -> None:
|
|
217
|
+
"""BPMベースのマーカー自動配置。"""
|
|
218
|
+
if not json_input:
|
|
219
|
+
raise click.UsageError("--json is required")
|
|
220
|
+
data = BeatMarkerInput.model_validate(json_input)
|
|
221
|
+
result = beat_marker_impl(
|
|
222
|
+
bpm=data.bpm,
|
|
223
|
+
clip_index=data.clip_index,
|
|
224
|
+
note_value=data.note_value,
|
|
225
|
+
color=data.color,
|
|
226
|
+
name=data.name,
|
|
227
|
+
duration=data.duration,
|
|
228
|
+
dry_run=dry_run,
|
|
229
|
+
)
|
|
230
|
+
output(result, pretty=ctx.obj.get("pretty"))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# --- Schema Registration ---
|
|
234
|
+
|
|
235
|
+
register_schema(
|
|
236
|
+
"timeline.marker.beats",
|
|
237
|
+
output_model=BeatMarkerOutput,
|
|
238
|
+
input_model=BeatMarkerInput,
|
|
239
|
+
)
|