video-edit-cli 0.1.0__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.
- video_edit_cli-0.1.0.dist-info/METADATA +68 -0
- video_edit_cli-0.1.0.dist-info/RECORD +37 -0
- video_edit_cli-0.1.0.dist-info/WHEEL +4 -0
- video_edit_cli-0.1.0.dist-info/entry_points.txt +3 -0
- video_editor/__init__.py +5 -0
- video_editor/assets.py +133 -0
- video_editor/audio/__init__.py +1 -0
- video_editor/audio/analysis.py +147 -0
- video_editor/audio/comparison.py +71 -0
- video_editor/audio/denoise.py +63 -0
- video_editor/audio/mastering.py +77 -0
- video_editor/cli.py +1021 -0
- video_editor/errors.py +33 -0
- video_editor/ffmpeg.py +48 -0
- video_editor/inspection.py +168 -0
- video_editor/media.py +92 -0
- video_editor/plans.py +90 -0
- video_editor/profiles.py +58 -0
- video_editor/provenance.py +49 -0
- video_editor/reframe.py +119 -0
- video_editor/rendering.py +328 -0
- video_editor/result.py +42 -0
- video_editor/review.py +169 -0
- video_editor/schemas/__init__.py +29 -0
- video_editor/schemas/edit-plan.schema.json +69 -0
- video_editor/schemas/project-profile.schema.json +86 -0
- video_editor/schemas/result.schema.json +42 -0
- video_editor/schemas/transcript.schema.json +49 -0
- video_editor/schemas/workspace.schema.json +39 -0
- video_editor/subtitles.py +137 -0
- video_editor/sync.py +149 -0
- video_editor/transcription/__init__.py +1 -0
- video_editor/transcription/base.py +73 -0
- video_editor/transcription/fixture.py +33 -0
- video_editor/transcription/mlx_whisper.py +68 -0
- video_editor/transcription/views.py +85 -0
- video_editor/workspace.py +105 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Derived transcript views: packed text for agent context, time-aligned search.
|
|
2
|
+
|
|
3
|
+
Transcript JSON is authoritative; these views are conveniences derived from it.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from video_editor import schemas
|
|
14
|
+
from video_editor.errors import InvalidInputError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_transcript(path: Path) -> dict[str, Any]:
|
|
18
|
+
if not path.is_file():
|
|
19
|
+
raise InvalidInputError(f"transcript file not found: {path}")
|
|
20
|
+
document: dict[str, Any] = json.loads(path.read_text())
|
|
21
|
+
schemas.validate(document, "transcript.schema.json")
|
|
22
|
+
return document
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def pack(document: dict[str, Any]) -> str:
|
|
26
|
+
"""Render a compact `[start-end] (speaker) text` line per segment."""
|
|
27
|
+
lines = [
|
|
28
|
+
f"# transcript of {document['source']['path']}",
|
|
29
|
+
f"# language={document['language']} backend={document['backend']} "
|
|
30
|
+
f"model={document['model']}",
|
|
31
|
+
]
|
|
32
|
+
for segment in document["segments"]:
|
|
33
|
+
speaker = f" ({segment['speaker']})" if segment.get("speaker") else ""
|
|
34
|
+
lines.append(
|
|
35
|
+
f"[{segment['start']:.2f}-{segment['end']:.2f}]{speaker} {segment['text'].strip()}"
|
|
36
|
+
)
|
|
37
|
+
return "\n".join(lines) + "\n"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _normalize(text: str) -> str:
|
|
41
|
+
return re.sub(r"[^\w]+", " ", text.lower()).strip()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def search(
|
|
45
|
+
document: dict[str, Any], query: str, max_results: int = 20
|
|
46
|
+
) -> list[dict[str, Any]]:
|
|
47
|
+
"""Find query occurrences in the word stream; return time-aligned matches."""
|
|
48
|
+
query_tokens = _normalize(query).split()
|
|
49
|
+
if not query_tokens:
|
|
50
|
+
raise InvalidInputError("search query is empty after normalization")
|
|
51
|
+
|
|
52
|
+
words: list[dict[str, Any]] = []
|
|
53
|
+
for segment_index, segment in enumerate(document["segments"]):
|
|
54
|
+
for word in segment["words"]:
|
|
55
|
+
token = _normalize(word["text"])
|
|
56
|
+
if token:
|
|
57
|
+
words.append(
|
|
58
|
+
{
|
|
59
|
+
"token": token,
|
|
60
|
+
"start": word["start"],
|
|
61
|
+
"end": word["end"],
|
|
62
|
+
"segment_index": segment_index,
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
matches: list[dict[str, Any]] = []
|
|
67
|
+
span = len(query_tokens)
|
|
68
|
+
for i in range(len(words) - span + 1):
|
|
69
|
+
window = words[i : i + span]
|
|
70
|
+
if [w["token"] for w in window] == query_tokens:
|
|
71
|
+
segment = document["segments"][window[0]["segment_index"]]
|
|
72
|
+
matches.append(
|
|
73
|
+
{
|
|
74
|
+
"start": window[0]["start"],
|
|
75
|
+
"end": window[-1]["end"],
|
|
76
|
+
"text": " ".join(w["token"] for w in window),
|
|
77
|
+
"segment_index": window[0]["segment_index"],
|
|
78
|
+
"segment_start": segment["start"],
|
|
79
|
+
"segment_end": segment["end"],
|
|
80
|
+
"segment_text": segment["text"].strip(),
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
if len(matches) >= max_results:
|
|
84
|
+
break
|
|
85
|
+
return matches
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Workspace: organized directories plus a manifest of immutable sources and artifacts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from video_editor import SCHEMA_VERSION, schemas
|
|
11
|
+
from video_editor.errors import InvalidInputError
|
|
12
|
+
from video_editor.provenance import sha256_file, utc_now
|
|
13
|
+
|
|
14
|
+
SUBDIRS = (
|
|
15
|
+
"sources",
|
|
16
|
+
"analysis",
|
|
17
|
+
"proxies",
|
|
18
|
+
"plans",
|
|
19
|
+
"previews",
|
|
20
|
+
"renders",
|
|
21
|
+
"reports",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def manifest_path(root: Path) -> Path:
|
|
26
|
+
return root / "workspace.json"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load(root: Path) -> dict[str, Any]:
|
|
30
|
+
path = manifest_path(root)
|
|
31
|
+
if not path.is_file():
|
|
32
|
+
raise InvalidInputError(f"no workspace manifest at {path}")
|
|
33
|
+
manifest: dict[str, Any] = json.loads(path.read_text())
|
|
34
|
+
schemas.validate(manifest, "workspace.schema.json")
|
|
35
|
+
return manifest
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def save(root: Path, manifest: dict[str, Any]) -> None:
|
|
39
|
+
schemas.validate(manifest, "workspace.schema.json")
|
|
40
|
+
manifest_path(root).write_text(json.dumps(manifest, indent=2) + "\n")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def init(
|
|
44
|
+
root: Path, sources: list[Path], roles: list[str] | None = None
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
"""Create workspace directories and register sources without modifying them."""
|
|
47
|
+
if manifest_path(root).exists():
|
|
48
|
+
raise InvalidInputError(f"workspace already exists at {root}")
|
|
49
|
+
role_list = roles or []
|
|
50
|
+
if role_list and len(role_list) != len(sources):
|
|
51
|
+
raise InvalidInputError("--role count must match --source count when provided")
|
|
52
|
+
|
|
53
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
for sub in SUBDIRS:
|
|
55
|
+
(root / sub).mkdir(exist_ok=True)
|
|
56
|
+
|
|
57
|
+
manifest: dict[str, Any] = {
|
|
58
|
+
"schema_version": SCHEMA_VERSION,
|
|
59
|
+
"workspace_id": uuid.uuid4().hex[:12],
|
|
60
|
+
"created_at": utc_now(),
|
|
61
|
+
"sources": [],
|
|
62
|
+
"artifacts": [],
|
|
63
|
+
}
|
|
64
|
+
for index, source in enumerate(sources):
|
|
65
|
+
source = source.resolve()
|
|
66
|
+
if not source.is_file():
|
|
67
|
+
raise InvalidInputError(f"source file not found: {source}")
|
|
68
|
+
source_id = f"src-{index + 1}"
|
|
69
|
+
manifest["sources"].append(
|
|
70
|
+
{
|
|
71
|
+
"id": source_id,
|
|
72
|
+
"path": str(source),
|
|
73
|
+
"sha256": sha256_file(source),
|
|
74
|
+
"role": role_list[index] if role_list else None,
|
|
75
|
+
"registered_at": utc_now(),
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
link = root / "sources" / f"{source_id}{source.suffix}"
|
|
79
|
+
if not link.exists():
|
|
80
|
+
link.symlink_to(source)
|
|
81
|
+
save(root, manifest)
|
|
82
|
+
return manifest
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def register_artifact(root: Path, path: Path, kind: str, command: str) -> None:
|
|
86
|
+
manifest = load(root)
|
|
87
|
+
manifest["artifacts"].append(
|
|
88
|
+
{
|
|
89
|
+
"path": str(path.resolve()),
|
|
90
|
+
"kind": kind,
|
|
91
|
+
"command": command,
|
|
92
|
+
"created_at": utc_now(),
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
save(root, manifest)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def resolve_source(root: Path, ref: str) -> Path:
|
|
99
|
+
"""Resolve a source id from the manifest to its immutable path."""
|
|
100
|
+
manifest = load(root)
|
|
101
|
+
for record in manifest["sources"]:
|
|
102
|
+
if record["id"] == ref:
|
|
103
|
+
return Path(record["path"])
|
|
104
|
+
known = ", ".join(record["id"] for record in manifest["sources"])
|
|
105
|
+
raise InvalidInputError(f"unknown source id '{ref}' (known: {known or 'none'})")
|