artdo-motion-review 0.1.0__tar.gz
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.
- artdo_motion_review-0.1.0/.gitignore +27 -0
- artdo_motion_review-0.1.0/LICENSE +4 -0
- artdo_motion_review-0.1.0/PKG-INFO +46 -0
- artdo_motion_review-0.1.0/README.md +35 -0
- artdo_motion_review-0.1.0/pyproject.toml +26 -0
- artdo_motion_review-0.1.0/src/artdo_motion_review/__init__.py +97 -0
- artdo_motion_review-0.1.0/tests/test_review.py +43 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Dependencies and local tool state
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnpm-store/
|
|
4
|
+
.DS_Store
|
|
5
|
+
|
|
6
|
+
# Local environment files
|
|
7
|
+
.env
|
|
8
|
+
.env.*
|
|
9
|
+
!.env.example
|
|
10
|
+
|
|
11
|
+
# Runtime output is local-only by default: JSON logs, helper scripts, screenshots,
|
|
12
|
+
# browser snapshots, network captures, and lock files all belong here.
|
|
13
|
+
runs/*
|
|
14
|
+
!runs/.gitkeep
|
|
15
|
+
|
|
16
|
+
# A human may deliberately promote a small, redacted JSON receipt as durable
|
|
17
|
+
# evidence. Promotion is explicit by path, not automatic from a runner.
|
|
18
|
+
!runs/evidence/
|
|
19
|
+
runs/evidence/*
|
|
20
|
+
!runs/evidence/README.md
|
|
21
|
+
!runs/evidence/*.json
|
|
22
|
+
|
|
23
|
+
# Target-size media exports are reproducible local cache files. Reviewed shared
|
|
24
|
+
# masters under assets/<site>/_shared/v*/ remain tracked. Add one export with
|
|
25
|
+
# `git add -f` only when an exact reviewed file must survive across machines for
|
|
26
|
+
# a registered publishing asset; never force-add the whole export cache.
|
|
27
|
+
assets/**/_shared/exports/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
LicenseRef-NoLicenseGrant: No package-use license is granted by this preparation.
|
|
2
|
+
The authorized publisher must confirm distribution rights and the intended
|
|
3
|
+
license before any registry upload. No ownership or exclusivity is asserted.
|
|
4
|
+
Original AI-assisted standalone code and documentation are supplied for review.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: artdo-motion-review
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Audit timestamped human observations for still-to-motion video candidates locally.
|
|
5
|
+
Project-URL: Homepage, https://artdo.ai/image-to-video
|
|
6
|
+
License-Expression: LicenseRef-NoLicenseGrant
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: manual-audit,motion,video-review
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# artdo-motion-review
|
|
13
|
+
|
|
14
|
+
A dependency-free Python library and local CLI for summarising timestamped human observations of still-to-motion video candidates. It groups observations by clip, sorts them by time, identifies failed checks and marks each clip as `reviewed`, `pending` or `revise`. It does not read video, inspect pixels, evaluate audio or contact an AI service.
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from artdo_motion_review import audit_observations
|
|
18
|
+
checks = dict(motion="pass", identity="pass", geometry="fail",
|
|
19
|
+
text="na", continuity="pass", audio="na")
|
|
20
|
+
report = audit_observations([dict(id="note-1", clipId="boat-v1", shotId="wide",
|
|
21
|
+
atMs=1250, checks=checks, rightsConfirmed=True, note="Fold changes near the bow")])
|
|
22
|
+
assert report["clips"][0]["status"] == "revise"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The example is fictional. Checks and rights confirmation are supplied human assertions, not measurements or legal clearance. `reviewed` describes only the entered observations; it never establishes full temporal coverage, visual quality, consent or fitness for release.
|
|
26
|
+
|
|
27
|
+
## Input contract
|
|
28
|
+
|
|
29
|
+
Supply 1–10,000 objects with unique nonempty `id`, `clipId` and `shotId` strings (at most 2,000 input characters). `atMs` must be an actual integer from 0 through 86,400,000; booleans, fractions and negatives are rejected. This bound belongs to the local audit utility, not ArtDo's supported clip lengths. The tool cannot verify that a timestamp exists in an actual clip.
|
|
30
|
+
|
|
31
|
+
`checks` must have exactly six keys: `motion`, `identity`, `geometry`, `text`, `continuity`, `audio`, with values `pending`, `pass`, `fail` or `na`. `rightsConfirmed` must be an actual boolean. `note` is optional text, limited to 2,000 characters. Other top-level fields are ignored. Any failure yields `revise`; unresolved checks, absent rights confirmation or all-N/A checks yield `pending`. Otherwise an observation is `reviewed`. Clip status uses the most conservative observation status. Inputs are not mutated.
|
|
32
|
+
|
|
33
|
+
`load_observations(text)` parses JSON and rejects duplicate keys, non-finite constants and malformed/deep nesting. `audit_observations(data)` validates its schema. The CLI reads one local UTF-8 file or stdin, up to 5 MiB, prints JSON and writes no file:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
artdo-motion-review observations.json
|
|
37
|
+
artdo-motion-review - < observations.json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Valid reports return status 0 even when they contain failures; malformed input returns 1 and argument errors return 2. Python 3.10+ is declared; the actual local test runtime is recorded in the publishing pack. Build/Hatchling/Twine are build tools, not runtime dependencies.
|
|
41
|
+
|
|
42
|
+
## Product context and provenance
|
|
43
|
+
|
|
44
|
+
The related [ArtDo image-to-video page](https://artdo.ai/image-to-video) offers an image-and-motion-prompt workflow with model-dependent settings and quoted credits. Check current options in that live product. This utility works independently and provides no generation SDK, private endpoint, model catalogue or guaranteed output. Do not enter private filenames or review notes into a public document without considering their contents.
|
|
45
|
+
|
|
46
|
+
Original AI-assisted code and documentation; no private product source, credentials or media are included. The included LicenseRef-NoLicenseGrant notice grants no package-use license or ownership assurance. Publisher identity, distribution rights, name/version availability and final upload remain execution checks. Local wheel/sdist preparation creates no PyPI or TestPyPI publication or reservation.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# artdo-motion-review
|
|
2
|
+
|
|
3
|
+
A dependency-free Python library and local CLI for summarising timestamped human observations of still-to-motion video candidates. It groups observations by clip, sorts them by time, identifies failed checks and marks each clip as `reviewed`, `pending` or `revise`. It does not read video, inspect pixels, evaluate audio or contact an AI service.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from artdo_motion_review import audit_observations
|
|
7
|
+
checks = dict(motion="pass", identity="pass", geometry="fail",
|
|
8
|
+
text="na", continuity="pass", audio="na")
|
|
9
|
+
report = audit_observations([dict(id="note-1", clipId="boat-v1", shotId="wide",
|
|
10
|
+
atMs=1250, checks=checks, rightsConfirmed=True, note="Fold changes near the bow")])
|
|
11
|
+
assert report["clips"][0]["status"] == "revise"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The example is fictional. Checks and rights confirmation are supplied human assertions, not measurements or legal clearance. `reviewed` describes only the entered observations; it never establishes full temporal coverage, visual quality, consent or fitness for release.
|
|
15
|
+
|
|
16
|
+
## Input contract
|
|
17
|
+
|
|
18
|
+
Supply 1–10,000 objects with unique nonempty `id`, `clipId` and `shotId` strings (at most 2,000 input characters). `atMs` must be an actual integer from 0 through 86,400,000; booleans, fractions and negatives are rejected. This bound belongs to the local audit utility, not ArtDo's supported clip lengths. The tool cannot verify that a timestamp exists in an actual clip.
|
|
19
|
+
|
|
20
|
+
`checks` must have exactly six keys: `motion`, `identity`, `geometry`, `text`, `continuity`, `audio`, with values `pending`, `pass`, `fail` or `na`. `rightsConfirmed` must be an actual boolean. `note` is optional text, limited to 2,000 characters. Other top-level fields are ignored. Any failure yields `revise`; unresolved checks, absent rights confirmation or all-N/A checks yield `pending`. Otherwise an observation is `reviewed`. Clip status uses the most conservative observation status. Inputs are not mutated.
|
|
21
|
+
|
|
22
|
+
`load_observations(text)` parses JSON and rejects duplicate keys, non-finite constants and malformed/deep nesting. `audit_observations(data)` validates its schema. The CLI reads one local UTF-8 file or stdin, up to 5 MiB, prints JSON and writes no file:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
artdo-motion-review observations.json
|
|
26
|
+
artdo-motion-review - < observations.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Valid reports return status 0 even when they contain failures; malformed input returns 1 and argument errors return 2. Python 3.10+ is declared; the actual local test runtime is recorded in the publishing pack. Build/Hatchling/Twine are build tools, not runtime dependencies.
|
|
30
|
+
|
|
31
|
+
## Product context and provenance
|
|
32
|
+
|
|
33
|
+
The related [ArtDo image-to-video page](https://artdo.ai/image-to-video) offers an image-and-motion-prompt workflow with model-dependent settings and quoted credits. Check current options in that live product. This utility works independently and provides no generation SDK, private endpoint, model catalogue or guaranteed output. Do not enter private filenames or review notes into a public document without considering their contents.
|
|
34
|
+
|
|
35
|
+
Original AI-assisted code and documentation; no private product source, credentials or media are included. The included LicenseRef-NoLicenseGrant notice grants no package-use license or ownership assurance. Publisher identity, distribution rights, name/version availability and final upload remain execution checks. Local wheel/sdist preparation creates no PyPI or TestPyPI publication or reservation.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "artdo-motion-review"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Audit timestamped human observations for still-to-motion video candidates locally."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "LicenseRef-NoLicenseGrant"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
dependencies = []
|
|
14
|
+
keywords = ["video-review", "motion", "manual-audit"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://artdo.ai/image-to-video"
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
artdo-motion-review = "artdo_motion_review:main"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/artdo_motion_review"]
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.sdist]
|
|
26
|
+
include = ["src/", "tests/", "README.md", "LICENSE", "pyproject.toml"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Local audits of human observations; never reads video or calls a service."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
CHECKS = ("motion", "identity", "geometry", "text", "continuity", "audio")
|
|
8
|
+
STATES = ("pending", "pass", "fail", "na")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _text(value, name):
|
|
12
|
+
if not isinstance(value, str) or not value.strip() or len(value) > 2000:
|
|
13
|
+
raise ValueError(f"{name} must be nonempty text of at most 2000 characters")
|
|
14
|
+
return value.strip()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def audit_observations(observations):
|
|
18
|
+
"""Validate 1-10000 observations and derive a per-clip human review summary."""
|
|
19
|
+
if not isinstance(observations, list) or not 1 <= len(observations) <= 10000:
|
|
20
|
+
raise ValueError("Supply a list of 1 to 10000 observations")
|
|
21
|
+
groups, seen = {}, set()
|
|
22
|
+
for index, item in enumerate(observations):
|
|
23
|
+
if not isinstance(item, dict):
|
|
24
|
+
raise ValueError(f"Observation {index} must be an object")
|
|
25
|
+
identifier = _text(item.get("id"), "id")
|
|
26
|
+
if identifier in seen:
|
|
27
|
+
raise ValueError(f"Duplicate observation id: {identifier}")
|
|
28
|
+
seen.add(identifier)
|
|
29
|
+
clip = _text(item.get("clipId"), "clipId")
|
|
30
|
+
shot = _text(item.get("shotId"), "shotId")
|
|
31
|
+
at = item.get("atMs")
|
|
32
|
+
if type(at) is not int or not 0 <= at <= 86400000:
|
|
33
|
+
raise ValueError("atMs must be an integer from 0 to 86400000")
|
|
34
|
+
checks = item.get("checks")
|
|
35
|
+
if not isinstance(checks, dict) or set(checks) != set(CHECKS):
|
|
36
|
+
raise ValueError(f"checks must contain exactly {CHECKS}")
|
|
37
|
+
if any(not isinstance(v, str) or v not in STATES for v in checks.values()):
|
|
38
|
+
raise ValueError("Invalid check state")
|
|
39
|
+
rights = item.get("rightsConfirmed")
|
|
40
|
+
if type(rights) is not bool:
|
|
41
|
+
raise ValueError("rightsConfirmed must be a boolean")
|
|
42
|
+
note = item.get("note", "")
|
|
43
|
+
if not isinstance(note, str) or len(note) > 2000:
|
|
44
|
+
raise ValueError("note must be text of at most 2000 characters")
|
|
45
|
+
failed = [name for name in CHECKS if checks[name] == "fail"]
|
|
46
|
+
unresolved = any(v == "pending" for v in checks.values())
|
|
47
|
+
status = "revise" if failed else "pending" if unresolved or not rights or "pass" not in checks.values() else "reviewed"
|
|
48
|
+
groups.setdefault(clip, []).append({"id": identifier, "shotId": shot, "atMs": at,
|
|
49
|
+
"status": status, "failedChecks": failed, "note": note.strip()})
|
|
50
|
+
clips = []
|
|
51
|
+
for clip, records in sorted(groups.items()):
|
|
52
|
+
records.sort(key=lambda r: (r["atMs"], r["id"]))
|
|
53
|
+
status = "revise" if any(r["status"] == "revise" for r in records) else "pending" if any(r["status"] == "pending" for r in records) else "reviewed"
|
|
54
|
+
clips.append({"clipId": clip, "status": status, "observations": records})
|
|
55
|
+
return {"schema": "motion-review-report/v1", "observationCount": len(observations), "clips": clips,
|
|
56
|
+
"counts": {status: sum(c["status"] == status for c in clips) for status in ("reviewed", "pending", "revise")}}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _unique_keys(pairs):
|
|
60
|
+
result = {}
|
|
61
|
+
for key, value in pairs:
|
|
62
|
+
if key in result:
|
|
63
|
+
raise ValueError(f"Duplicate JSON key: {key}")
|
|
64
|
+
result[key] = value
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_observations(raw):
|
|
69
|
+
"""Parse strict JSON. The returned value is validated by audit_observations."""
|
|
70
|
+
try:
|
|
71
|
+
return json.loads(raw, object_pairs_hook=_unique_keys,
|
|
72
|
+
parse_constant=lambda value: (_ for _ in ()).throw(ValueError(f"Invalid JSON constant: {value}")))
|
|
73
|
+
except (json.JSONDecodeError, RecursionError) as exc:
|
|
74
|
+
raise ValueError("Invalid or excessively nested JSON") from exc
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main(argv=None):
|
|
78
|
+
parser = argparse.ArgumentParser(description="Audit local timestamped human video observations")
|
|
79
|
+
parser.add_argument("file", help="UTF-8 JSON file, or - for stdin")
|
|
80
|
+
args = parser.parse_args(argv)
|
|
81
|
+
try:
|
|
82
|
+
if args.file == "-":
|
|
83
|
+
raw = sys.stdin.buffer.read(5 * 1024 * 1024 + 1)
|
|
84
|
+
else:
|
|
85
|
+
with open(args.file, "rb") as stream:
|
|
86
|
+
raw = stream.read(5 * 1024 * 1024 + 1)
|
|
87
|
+
if len(raw) > 5 * 1024 * 1024:
|
|
88
|
+
raise ValueError("Input exceeds 5 MiB")
|
|
89
|
+
print(json.dumps(audit_observations(load_observations(raw.decode("utf-8"))), indent=2, ensure_ascii=True))
|
|
90
|
+
return 0
|
|
91
|
+
except (ValueError, OSError, UnicodeError) as exc:
|
|
92
|
+
print(f"motion-review: {exc}", file=sys.stderr)
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import unittest
|
|
3
|
+
from artdo_motion_review import audit_observations, load_observations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def observation(**changes):
|
|
7
|
+
result = dict(id="a", clipId="clip", shotId="wide", atMs=1500,
|
|
8
|
+
checks=dict(motion="pass", identity="pass", geometry="pass", text="na", continuity="pass", audio="na"),
|
|
9
|
+
rightsConfirmed=True)
|
|
10
|
+
result.update(changes)
|
|
11
|
+
return result
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ReviewTests(unittest.TestCase):
|
|
15
|
+
def test_group_sort_conservative_and_immutable(self):
|
|
16
|
+
later = observation()
|
|
17
|
+
earlier = observation(id="b", atMs=0)
|
|
18
|
+
earlier["checks"]["geometry"] = "fail"
|
|
19
|
+
source = [later, earlier, observation(id="c", clipId="other")]
|
|
20
|
+
before = copy.deepcopy(source)
|
|
21
|
+
report = audit_observations(source)
|
|
22
|
+
self.assertEqual(report["counts"], dict(reviewed=1, pending=0, revise=1))
|
|
23
|
+
self.assertEqual(report["clips"][0]["observations"][0]["failedChecks"], ["geometry"])
|
|
24
|
+
self.assertEqual(source, before)
|
|
25
|
+
|
|
26
|
+
def test_pending_rights_and_all_na(self):
|
|
27
|
+
for record in [observation(rightsConfirmed=False), observation(checks={k:"na" for k in observation()["checks"]}), observation(checks={k:"pending" for k in observation()["checks"]})]:
|
|
28
|
+
self.assertEqual(audit_observations([record])["counts"]["pending"], 1)
|
|
29
|
+
|
|
30
|
+
def test_bad_inputs(self):
|
|
31
|
+
for value in [-1, True, 1.2, 86400001, None]:
|
|
32
|
+
with self.assertRaises(ValueError): audit_observations([observation(atMs=value)])
|
|
33
|
+
for value in [None, [], [observation(), observation()], [observation(rightsConfirmed="true")], [observation(checks={})], [observation(id=" ")]]:
|
|
34
|
+
with self.assertRaises(ValueError): audit_observations(value)
|
|
35
|
+
bad=observation(); bad["checks"]["motion"]=[]
|
|
36
|
+
with self.assertRaises(ValueError): audit_observations([bad])
|
|
37
|
+
|
|
38
|
+
def test_strict_json(self):
|
|
39
|
+
for raw in ['{"a":1,"a":2}', '[NaN]', '[Infinity]', '[']:
|
|
40
|
+
with self.assertRaises(ValueError): load_observations(raw)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__": unittest.main()
|