hugpy-video 0.2.3__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.
Files changed (109) hide show
  1. hugpy_video/__init__.py +33 -0
  2. hugpy_video/chat_video/__init__.py +1 -0
  3. hugpy_video/chat_video/video_analyzer.py +174 -0
  4. hugpy_video/cli.py +130 -0
  5. hugpy_video/config.py +79 -0
  6. hugpy_video/hooks.py +152 -0
  7. hugpy_video/intel/__init__.py +10 -0
  8. hugpy_video/intel/audio_schema.py +44 -0
  9. hugpy_video/intel/chains.py +174 -0
  10. hugpy_video/intel/crop_schema.py +53 -0
  11. hugpy_video/intel/ffprobe.py +28 -0
  12. hugpy_video/intel/frame_schema.py +81 -0
  13. hugpy_video/intel/gen_schema.py +132 -0
  14. hugpy_video/intel/identity_from_video_schema.py +118 -0
  15. hugpy_video/intel/identity_profiles.py +2182 -0
  16. hugpy_video/intel/identity_reconstruction_schema.py +567 -0
  17. hugpy_video/intel/identity_video_extract_schema.py +180 -0
  18. hugpy_video/intel/job_bridge.py +345 -0
  19. hugpy_video/intel/job_lifecycle.py +1188 -0
  20. hugpy_video/intel/job_schema.py +149 -0
  21. hugpy_video/intel/media_bus.py +2496 -0
  22. hugpy_video/intel/media_schema.py +98 -0
  23. hugpy_video/intel/media_store.py +216 -0
  24. hugpy_video/intel/mlt_render_schema.py +215 -0
  25. hugpy_video/intel/movie_schema.py +325 -0
  26. hugpy_video/intel/net.py +16 -0
  27. hugpy_video/intel/placement.py +203 -0
  28. hugpy_video/intel/plane.py +39 -0
  29. hugpy_video/intel/presets.py +645 -0
  30. hugpy_video/intel/prompt_intent.py +234 -0
  31. hugpy_video/intel/prompt_seeds.py +256 -0
  32. hugpy_video/intel/prompt_spread.py +880 -0
  33. hugpy_video/intel/reservation/__init__.py +67 -0
  34. hugpy_video/intel/reservation/engine.py +832 -0
  35. hugpy_video/intel/reservation/registry.py +348 -0
  36. hugpy_video/intel/reservation/templates.py +467 -0
  37. hugpy_video/intel/result_schema.py +67 -0
  38. hugpy_video/intel/runners/__init__.py +81 -0
  39. hugpy_video/intel/runners/_gpu_guard.py +119 -0
  40. hugpy_video/intel/runners/_img2img.py +137 -0
  41. hugpy_video/intel/runners/ffmpeg_audio.py +96 -0
  42. hugpy_video/intel/runners/ffmpeg_crop.py +120 -0
  43. hugpy_video/intel/runners/ffmpeg_frames.py +148 -0
  44. hugpy_video/intel/runners/identity_from_video.py +386 -0
  45. hugpy_video/intel/runners/identity_mesh.py +221 -0
  46. hugpy_video/intel/runners/identity_reconstruction.py +337 -0
  47. hugpy_video/intel/runners/identity_render_client.py +298 -0
  48. hugpy_video/intel/runners/identity_render_relay.py +657 -0
  49. hugpy_video/intel/runners/identity_video_extract_relay.py +439 -0
  50. hugpy_video/intel/runners/imagegen.py +293 -0
  51. hugpy_video/intel/runners/mlt_render.py +472 -0
  52. hugpy_video/intel/runners/movie.py +622 -0
  53. hugpy_video/intel/runners/scene.py +693 -0
  54. hugpy_video/intel/runners/sidecar.py +28 -0
  55. hugpy_video/intel/runners/studio_i2v.py +1116 -0
  56. hugpy_video/intel/runners/studio_movie.py +1348 -0
  57. hugpy_video/intel/runners/studio_placement.py +221 -0
  58. hugpy_video/intel/runners/studio_tester.py +26 -0
  59. hugpy_video/intel/runners/tts_chatterbox.py +85 -0
  60. hugpy_video/intel/scene_schema.py +161 -0
  61. hugpy_video/intel/shot_intent.py +127 -0
  62. hugpy_video/intel/studio/__init__.py +114 -0
  63. hugpy_video/intel/studio/artifacts.py +25 -0
  64. hugpy_video/intel/studio/editor_handoff.py +171 -0
  65. hugpy_video/intel/studio/enums.py +173 -0
  66. hugpy_video/intel/studio/env.py +77 -0
  67. hugpy_video/intel/studio/errors.py +170 -0
  68. hugpy_video/intel/studio/job.py +469 -0
  69. hugpy_video/intel/studio/manifest.py +567 -0
  70. hugpy_video/intel/studio/models_seed.py +538 -0
  71. hugpy_video/intel/studio/movie_plan.py +314 -0
  72. hugpy_video/intel/studio/presence.py +97 -0
  73. hugpy_video/intel/studio/presets.py +707 -0
  74. hugpy_video/intel/studio/produce.py +355 -0
  75. hugpy_video/intel/studio/registry.py +283 -0
  76. hugpy_video/intel/studio/router.py +486 -0
  77. hugpy_video/intel/studio/runners/__init__.py +20 -0
  78. hugpy_video/intel/studio/runners/cancel.py +18 -0
  79. hugpy_video/intel/studio/runners/ffmpeg_enhance.py +362 -0
  80. hugpy_video/intel/studio/runners/ltx_upscale.py +134 -0
  81. hugpy_video/intel/studio/runners/rife_interpolate.py +117 -0
  82. hugpy_video/intel/studio/runners/source.py +10 -0
  83. hugpy_video/intel/studio/runners/synthetic.py +524 -0
  84. hugpy_video/intel/studio/runners/wan_i2v.py +2026 -0
  85. hugpy_video/intel/studio/runners/wan_t2v.py +61 -0
  86. hugpy_video/intel/studio/runners/wan_vace.py +696 -0
  87. hugpy_video/intel/studio/schemas.py +515 -0
  88. hugpy_video/intel/studio/storage.py +40 -0
  89. hugpy_video/intel/studio/tester.py +542 -0
  90. hugpy_video/intel/studio_movie_schema.py +699 -0
  91. hugpy_video/intel/studio_presets.py +512 -0
  92. hugpy_video/jobs.py +74 -0
  93. hugpy_video/plugin.py +61 -0
  94. hugpy_video/plugin_builders.py +54 -0
  95. hugpy_video/py.typed +0 -0
  96. hugpy_video/schemas/__init__.py +0 -0
  97. hugpy_video/schemas/video_schemas.py +45 -0
  98. hugpy_video/selftest.py +92 -0
  99. hugpy_video/state.py +111 -0
  100. hugpy_video/studio_assist_log.py +434 -0
  101. hugpy_video/video_gen/__init__.py +9 -0
  102. hugpy_video/video_gen/schemas.py +71 -0
  103. hugpy_video/video_gen/video_gen_runner.py +100 -0
  104. hugpy_video-0.2.3.dist-info/METADATA +104 -0
  105. hugpy_video-0.2.3.dist-info/RECORD +109 -0
  106. hugpy_video-0.2.3.dist-info/WHEEL +5 -0
  107. hugpy_video-0.2.3.dist-info/entry_points.txt +5 -0
  108. hugpy_video-0.2.3.dist-info/licenses/LICENSE +41 -0
  109. hugpy_video-0.2.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,33 @@
1
+ """hugpy-video: execution and artifacts for Hugpy video work.
2
+
3
+ Owns the media library jail, the sqlite media job bus and its lifecycle
4
+ bridge, the frozen job specs and presets, the GPU reservation engine, the
5
+ ffmpeg/diffusers/studio/identity runners and the studio spine
6
+ (``intel``), the chat-side video analyzer (``chat_video``), the engine
7
+ task runner for text-/image-to-video (``video_gen``) and the studio assist
8
+ log. Decisions and plans belong to ``hugpy_oracle``, which plugs in through
9
+ ``hugpy_video.hooks`` (prompt coordination) and ``hugpy_video.jobs``
10
+ (``register_job``) — this package never imports the oracle, the fleet or
11
+ the server.
12
+
13
+ Import-light: nothing here pulls torch/ffmpeg/studio stacks. Sub-modules are
14
+ imported explicitly by callers (``hugpy_video.jobs``, ``hugpy_video.hooks``,
15
+ ``hugpy_video.plugin``, ``hugpy_video.state``, ``hugpy_video.config``).
16
+
17
+ Version is single-sourced from pyproject.toml via importlib.metadata.
18
+ """
19
+
20
+ from importlib.metadata import PackageNotFoundError, version
21
+
22
+ try:
23
+ __version__ = version("hugpy-video")
24
+ except PackageNotFoundError: # running from a checkout without install
25
+ __version__ = "0.0.0+unknown"
26
+
27
+ from hugpy_video.config import Config, load_config # stdlib-only, cheap
28
+
29
+ __all__ = [
30
+ "__version__",
31
+ "Config",
32
+ "load_config",
33
+ ]
@@ -0,0 +1 @@
1
+ """Chat-side video helpers (old ``managers/video``). Import-light; see ``video_analyzer``."""
@@ -0,0 +1,174 @@
1
+ """Frame-by-frame video analysis over a vision runner (chat-side helper).
2
+
3
+ Import-light: the vision runner/schemas come from ``hugpy_media`` (allowed),
4
+ the no-think helpers from ``hugpy_engine`` (lazy, inside the loop).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import copy
9
+ import os.path as osp
10
+ import time
11
+ import uuid
12
+ from typing import Any, Union
13
+
14
+ from abstract_essentials import safe_dump_to_file, safe_load_from_json
15
+ from hugpy_media.vision.schemas import VisionRequest
16
+ from hugpy_media.vision.vision_runner import VisionRunner
17
+ from hugpy_platform.utils import get_base_64_image, require_file
18
+
19
+ from hugpy_video.schemas.video_schemas import (
20
+ FrameAnalysis,
21
+ VideoAnalysisConfig,
22
+ VideoAnalysisSummary,
23
+ )
24
+
25
+ def _resolve_manifest_path(source: Union[str, Any]) -> str:
26
+ if isinstance(source, str):
27
+ return source
28
+ p = getattr(source, "manifest_path", None)
29
+ if isinstance(p, str):
30
+ return p
31
+ raise TypeError(
32
+ f"expected manifest path str or object with .manifest_path, "
33
+ f"got {type(source).__name__}"
34
+ )
35
+
36
+ def _build_prompt(user_prompt: str, frame_context: dict) -> str:
37
+ return (
38
+ "-----------prompt---------\n"
39
+ f"{user_prompt}\n"
40
+ "-----------frame context---------\n"
41
+ f"{frame_context}\n"
42
+ )
43
+
44
+
45
+ async def analyze_video(
46
+ source: Union[str, Any],
47
+ runner: VisionRunner,
48
+ config: VideoAnalysisConfig,
49
+ ) -> VideoAnalysisSummary:
50
+ manifest_path = require_file(_resolve_manifest_path(source), "manifest_path")
51
+ manifest_data = safe_load_from_json(manifest_path)
52
+ if not isinstance(manifest_data, dict):
53
+ raise ValueError(f"manifest is not a dict: {manifest_path}")
54
+
55
+ workspace_dir = manifest_data.get("workspace_dir")
56
+ if not workspace_dir or not osp.isdir(workspace_dir):
57
+ raise FileNotFoundError(f"workspace_dir invalid: {workspace_dir!r}")
58
+
59
+ files = manifest_data.get("files") or {}
60
+ frame_context_path = require_file(files.get("frame_context"), "files.frame_context")
61
+
62
+ frame_contexts = safe_load_from_json(frame_context_path)
63
+ if not isinstance(frame_contexts, list) or not frame_contexts:
64
+ raise ValueError(
65
+ f"frame_context must be a non-empty list, "
66
+ f"got {type(frame_contexts).__name__}"
67
+ )
68
+
69
+ total_frames = len(frame_contexts)
70
+ last = frame_contexts[-1]
71
+ total_video_length = last.get("timestamp") if isinstance(last, dict) else None
72
+
73
+ analysis_json_path = osp.join(workspace_dir, "analysis.json")
74
+ manifest_data.setdefault("files", {})["analysis_json"] = analysis_json_path
75
+ safe_dump_to_file(data=manifest_data, file_path=manifest_path)
76
+
77
+ # Resume keyed by frame_path so reruns are idempotent.
78
+ done_by_frame: dict[str, dict] = {}
79
+ if config.resume and osp.isfile(analysis_json_path):
80
+ prior = safe_load_from_json(analysis_json_path) or []
81
+ for entry in prior:
82
+ fp = entry.get("frame_path")
83
+ if fp and entry.get("error") is None:
84
+ done_by_frame[fp] = entry
85
+
86
+ records: list[dict] = list(done_by_frame.values())
87
+ model_key = runner.cfg.model_key # runner is the source of truth
88
+
89
+ for i, raw_ctx in enumerate(frame_contexts):
90
+ if not isinstance(raw_ctx, dict):
91
+ records.append({
92
+ "frame_index": i,
93
+ "error": f"frame_context[{i}] is not a dict",
94
+ })
95
+ continue
96
+
97
+ ctx = copy.deepcopy(raw_ctx) # never mutate caller's data
98
+ frame_path = ctx.get("frame_path")
99
+
100
+ if not frame_path or not osp.isfile(frame_path):
101
+ err = f"frame_path missing or not found: {frame_path!r}"
102
+ if config.raise_on_frame_error:
103
+ raise FileNotFoundError(err)
104
+ ctx.update({"frame_index": i, "error": err})
105
+ records.append(ctx)
106
+ continue
107
+
108
+ if frame_path in done_by_frame:
109
+ continue
110
+
111
+ ctx["frame_index"] = i
112
+ ctx["total_frames"] = total_frames
113
+ ctx["total_video_length"] = total_video_length
114
+ # NO-THINK (utils/no_think.py). Every frame's `analysis` is VALIDATED and
115
+ # PERSISTED to analysis.json — a monologue here poisons the whole dataset,
116
+ # and nothing downstream re-reads the frame to notice. The reasoning is
117
+ # kept alongside (FrameAnalysis is extra="allow") rather than discarded.
118
+ from hugpy_engine.utils.no_think import with_no_think, strip_think
119
+ rendered_prompt = with_no_think(_build_prompt(config.prompt, ctx))
120
+ image_b64 = get_base_64_image(frame_path)
121
+ req = VisionRequest(
122
+ request_id=f"frame-{i}-{uuid.uuid4().hex[:8]}",
123
+ model_key=model_key,
124
+ prompt=rendered_prompt,
125
+ max_new_tokens=config.max_new_tokens,
126
+ max_tokens=config.max_tokens,
127
+ image_b64=image_b64,
128
+ )
129
+
130
+ t0 = time.time()
131
+ try:
132
+ vresult = await runner.run(req)
133
+ text, err = vresult.text, vresult.error
134
+ except Exception as e:
135
+ if config.raise_on_frame_error:
136
+ raise
137
+ text, err = None, f"{type(e).__name__}: {e}"
138
+ duration = time.time() - t0
139
+
140
+ reasoning = ""
141
+ if text:
142
+ text, reasoning = strip_think(text)
143
+ if not text:
144
+ # Nothing but thinking — record it as an error rather than
145
+ # persisting an empty analysis that reads like a clean run.
146
+ err = err or ("model returned only reasoning and no analysis "
147
+ "(ignored the no-think directive)")
148
+
149
+ ctx.update({
150
+ "analysis_prompt": rendered_prompt,
151
+ "analysis": text,
152
+ "analysis_reasoning": reasoning,
153
+ "model_key": model_key,
154
+ "analysis_duration": duration,
155
+ "error": err,
156
+ })
157
+
158
+ # Validate-on-write: schema drift fails fast, not three days from now
159
+ FrameAnalysis.model_validate(ctx)
160
+ records.append(ctx)
161
+
162
+ if (i + 1) % config.save_every == 0 or (i + 1) == total_frames:
163
+ safe_dump_to_file(data=records, file_path=analysis_json_path)
164
+
165
+ succeeded = sum(1 for r in records if r.get("error") is None)
166
+ failed = sum(1 for r in records if r.get("error") is not None)
167
+
168
+ return VideoAnalysisSummary(
169
+ manifest_path=manifest_path,
170
+ analysis_json_path=analysis_json_path,
171
+ frames_total=total_frames,
172
+ frames_succeeded=succeeded,
173
+ frames_failed=failed,
174
+ )
hugpy_video/cli.py ADDED
@@ -0,0 +1,130 @@
1
+ """``hugpy-video`` command line (minimal; the console script in pyproject).
2
+
3
+ hugpy-video --help
4
+ hugpy-video jobs list [--all] [--limit N] [--json]
5
+ hugpy-video jobs registry
6
+ hugpy-video selftest # in-process checks, no GPU/service
7
+ hugpy-video models audit --json # declared model roster and readiness gates
8
+
9
+ Everything heavy is imported inside the sub-command that needs it.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ from typing import List, Optional
18
+
19
+
20
+ def _cmd_jobs_list(args) -> int:
21
+ from hugpy_video import jobs
22
+
23
+ rows = jobs.list_jobs(include_terminal=args.all, limit=args.limit)
24
+ if args.json:
25
+ print(json.dumps(rows, indent=2, default=str))
26
+ return 0
27
+ if not rows:
28
+ print("no jobs")
29
+ return 0
30
+ for r in rows:
31
+ print(f"{r.get('job_id','?'):34} {r.get('name','?'):26} {r.get('status','?')}")
32
+ return 0
33
+
34
+
35
+ def _cmd_jobs_registry(args) -> int:
36
+ from hugpy_video import jobs
37
+
38
+ for name, spec in sorted(jobs.registered_jobs().items()):
39
+ print(f"{name:26} {spec.runner_key[0]}/{spec.runner_key[1]:18} "
40
+ f"queue={spec.queue} timeout={spec.timeout_s}s")
41
+ return 0
42
+
43
+
44
+ def _cmd_selftest(args) -> int:
45
+ from hugpy_video.selftest import run_selftest
46
+
47
+ failures = run_selftest(verbose=True)
48
+ return 1 if failures else 0
49
+
50
+
51
+ def _cmd_state(args) -> int:
52
+ from dataclasses import asdict
53
+
54
+ from hugpy_video.state import get_state_roots
55
+
56
+ print(json.dumps(asdict(get_state_roots()), indent=2))
57
+ return 0
58
+
59
+
60
+ def _cmd_models_audit(args) -> int:
61
+ """Inventory every declared model without loading weights or allocating a GPU."""
62
+ from hugpy_video.intel.studio.registry import MODEL_REGISTRY, model_gate_reasons
63
+
64
+ rows = []
65
+ for model_id, cfg in sorted(MODEL_REGISTRY.items()):
66
+ precision_gb = {precision.value: gb for precision, gb in cfg.vram.per_precision}
67
+ rows.append({
68
+ "model_id": model_id,
69
+ "family": cfg.family.value,
70
+ "capabilities": sorted(cap.value for cap in cfg.capabilities),
71
+ "tasks": sorted(task.value for task in cfg.tasks),
72
+ "minimum_vram_gb": min(precision_gb.values()) if precision_gb else None,
73
+ "vram_by_precision_gb": precision_gb,
74
+ "weights_pinned": cfg.weight_hash is not None,
75
+ "runner_gaps": model_gate_reasons(model_id),
76
+ })
77
+ if args.json:
78
+ print(json.dumps({"models": rows}, indent=2, sort_keys=True))
79
+ else:
80
+ for row in rows:
81
+ status = ", ".join(row["runner_gaps"].values()) or "runner present"
82
+ pin = "pinned" if row["weights_pinned"] else "unpinned"
83
+ print(f"{row['model_id']:32} {row['minimum_vram_gb']!s:>6} GB {pin:8} {status}")
84
+ print(f"{len(rows)} declared models; audit does not prove installed weights or GPU readiness")
85
+ return 0
86
+
87
+
88
+ def build_parser() -> argparse.ArgumentParser:
89
+ from hugpy_video import __version__
90
+
91
+ p = argparse.ArgumentParser(prog="hugpy-video",
92
+ description="Hugpy video: media job bus, runners, studio.")
93
+ p.add_argument("--version", action="version", version=f"hugpy-video {__version__}")
94
+ sub = p.add_subparsers(dest="cmd")
95
+
96
+ jobs = sub.add_parser("jobs", help="media job bus")
97
+ jsub = jobs.add_subparsers(dest="jobs_cmd")
98
+ ls = jsub.add_parser("list", help="list jobs")
99
+ ls.add_argument("--all", action="store_true", help="include terminal jobs")
100
+ ls.add_argument("--limit", type=int, default=50)
101
+ ls.add_argument("--json", action="store_true")
102
+ ls.set_defaults(func=_cmd_jobs_list)
103
+ reg = jsub.add_parser("registry", help="registered job names and runner keys")
104
+ reg.set_defaults(func=_cmd_jobs_registry)
105
+
106
+ st = sub.add_parser("selftest", help="in-process self checks (no GPU, no service)")
107
+ st.set_defaults(func=_cmd_selftest)
108
+
109
+ state = sub.add_parser("state", help="print the resolved state roots")
110
+ state.set_defaults(func=_cmd_state)
111
+ models = sub.add_parser("models", help="studio model inventory")
112
+ msub = models.add_subparsers(dest="models_cmd")
113
+ audit = msub.add_parser("audit", help="report runner, pin and VRAM requirements")
114
+ audit.add_argument("--json", action="store_true")
115
+ audit.set_defaults(func=_cmd_models_audit)
116
+ return p
117
+
118
+
119
+ def main(argv: Optional[List[str]] = None) -> int:
120
+ parser = build_parser()
121
+ args = parser.parse_args(argv)
122
+ func = getattr(args, "func", None)
123
+ if func is None:
124
+ parser.print_help()
125
+ return 0
126
+ return int(func(args) or 0)
127
+
128
+
129
+ if __name__ == "__main__": # pragma: no cover
130
+ sys.exit(main())
hugpy_video/config.py ADDED
@@ -0,0 +1,79 @@
1
+ """Canonical hugpy credential/config loader — the single-source-of-truth seam.
2
+
3
+ Precedence (same contract as hugpy_agent.config, later layer wins):
4
+ agent.toml -> .env (workspace/CWD) -> process env -> explicit overrides
5
+
6
+ Canonical variable names (frozen per ARMS-ASSESSMENT-2026-08-20):
7
+ HUGPY_API_KEY the hp_ public API key
8
+ HUGPY_BASE central base URL (default https://dev.hugpy.ai/api)
9
+
10
+ Legacy aliases are accepted, lowest priority within their layer, and recorded in
11
+ Config.deprecations so callers can warn. New code must never read the aliases.
12
+ Fleet-scope store: /srv/vm_mgr/secrets/hugpy.env (0600) — pass as env_file.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+
21
+ DEFAULT_BASE = "https://dev.hugpy.ai/api"
22
+
23
+ # alias -> canonical; order matters only for reporting
24
+ _KEY_ALIASES = ("CONSOLE_API", "HUGPY_API_TOKEN", "HUGPY_AGENT_API", "HUGPY_API_KEY_NEWVM")
25
+ _BASE_ALIASES = ("HUGPY_BASE_URL", "HUGPY_URL", "HUGPY_CENTRAL")
26
+
27
+
28
+ @dataclass
29
+ class Config:
30
+ base: str = DEFAULT_BASE
31
+ api_key: str = ""
32
+ sources: dict = field(default_factory=dict) # attr -> winning layer
33
+ deprecations: list = field(default_factory=list) # (alias, layer) hits
34
+
35
+
36
+ def _parse_env_file(path: Path) -> dict:
37
+ out = {}
38
+ try:
39
+ for line in path.read_text().splitlines():
40
+ line = line.strip()
41
+ if not line or line.startswith("#") or "=" not in line:
42
+ continue
43
+ k, _, v = line.partition("=")
44
+ out[k.strip()] = v.strip().strip("'\"")
45
+ except OSError:
46
+ pass
47
+ return out
48
+
49
+
50
+ def _apply(cfg: Config, mapping: dict, layer: str) -> None:
51
+ # aliases first so a canonical name in the same layer wins
52
+ for alias in _KEY_ALIASES:
53
+ if mapping.get(alias):
54
+ cfg.api_key = mapping[alias]
55
+ cfg.sources["api_key"] = layer
56
+ cfg.deprecations.append((alias, layer))
57
+ for alias in _BASE_ALIASES:
58
+ if mapping.get(alias):
59
+ cfg.base = mapping[alias]
60
+ cfg.sources["base"] = layer
61
+ cfg.deprecations.append((alias, layer))
62
+ if mapping.get("HUGPY_API_KEY"):
63
+ cfg.api_key = mapping["HUGPY_API_KEY"]
64
+ cfg.sources["api_key"] = layer
65
+ if mapping.get("HUGPY_BASE"):
66
+ cfg.base = mapping["HUGPY_BASE"]
67
+ cfg.sources["base"] = layer
68
+
69
+
70
+ def load_config(env_file: str | os.PathLike | None = None, **overrides) -> Config:
71
+ cfg = Config()
72
+ if env_file:
73
+ _apply(cfg, _parse_env_file(Path(env_file)), f".env:{env_file}")
74
+ _apply(cfg, dict(os.environ), "env")
75
+ for attr, value in overrides.items():
76
+ if value:
77
+ setattr(cfg, attr, value)
78
+ cfg.sources[attr] = "override"
79
+ return cfg
hugpy_video/hooks.py ADDED
@@ -0,0 +1,152 @@
1
+ """Upper-layer callbacks video needs but must not import (EXTRACTION_GUIDE §3.3).
2
+
3
+ ``hugpy_video`` executes immutable specs; ``hugpy_oracle`` decides. Two places
4
+ in video used to reach *up* into the oracle's ``prompt_coordination`` module:
5
+
6
+ * ``intel.prompt_spread.coordinate_spread`` — after a spread reply is parsed,
7
+ review the rows' words against their knobs and apply the mechanical
8
+ decisions (joint mode, parent pointer, seed, frame count, identity refs).
9
+ * ``intel.studio.movie_plan.preflight_coordination`` /
10
+ ``coordination_report`` — the submit-time words-vs-knobs review of a
11
+ ``StudioMovieSpec``.
12
+
13
+ Both now call the :class:`PromptCoordinator` installed here. The default is a
14
+ no-op coordinator whose report says *nothing was reviewed* (``reviewed: False``)
15
+ — never a silent "reviewed and fine". The oracle installs its implementation at
16
+ composition time (``hugpy_server`` wiring, or an oracle CLI)::
17
+
18
+ from hugpy_video.hooks import set_prompt_coordinator
19
+ from hugpy_oracle.relay import prompt_coordination as PC
20
+
21
+ class OracleCoordinator:
22
+ block_confidence = PC.BLOCK_CONFIDENCE
23
+ def review(self, rows, *, notes="", context=None, llm=None):
24
+ return PC.review(rows, notes=notes, context=context, llm=llm)
25
+ def review_goals(self, spec, *, context=None):
26
+ return PC.review_goals(spec, context=context)
27
+ def apply_decisions(self, rows, report):
28
+ return PC.apply_decisions(rows, report)
29
+ def blocking_mismatches(self, report, threshold=None):
30
+ return PC.blocking_mismatches(
31
+ report, PC.BLOCK_CONFIDENCE if threshold is None else threshold)
32
+
33
+ set_prompt_coordinator(OracleCoordinator())
34
+
35
+ Report objects are opaque to video: it only calls ``as_dict()`` on them and
36
+ reads ``segment_id`` / ``knob`` off the applied decisions. Invariant 9 (no
37
+ generated prompt text crosses rows) is the coordinator's to keep; the null
38
+ coordinator trivially keeps it by writing nothing.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ from typing import Any, Dict, List, Mapping, Optional, Protocol, Sequence, Tuple, runtime_checkable
44
+
45
+ __all__ = [
46
+ "CoordinationReportLike",
47
+ "KnobDecisionLike",
48
+ "PromptCoordinator",
49
+ "NullPromptCoordinator",
50
+ "NullCoordinationReport",
51
+ "get_prompt_coordinator",
52
+ "set_prompt_coordinator",
53
+ "reset_hooks",
54
+ ]
55
+
56
+
57
+ @runtime_checkable
58
+ class CoordinationReportLike(Protocol):
59
+ """What video reads off a review report: its wire shape."""
60
+
61
+ def as_dict(self) -> Dict[str, Any]: ...
62
+
63
+
64
+ @runtime_checkable
65
+ class KnobDecisionLike(Protocol):
66
+ """One applied decision: which row, which knob (the value is read off the row)."""
67
+
68
+ segment_id: str
69
+ knob: str
70
+
71
+
72
+ @runtime_checkable
73
+ class PromptCoordinator(Protocol):
74
+ """Words-vs-knobs review for a prompt set. Implemented by ``hugpy_oracle``."""
75
+
76
+ #: Confidence at or above which a ``mismatch`` blocks a submit.
77
+ block_confidence: float
78
+
79
+ def review(self, rows: Sequence[Mapping[str, Any]], *, notes: str = "",
80
+ context: Optional[Mapping[str, Any]] = None,
81
+ llm: Any = None) -> CoordinationReportLike:
82
+ """Review generated/enhanced prompt rows (``segment_id``, ``prompt``,
83
+ ``locked`` + current knobs). Every row must appear in the report."""
84
+ ...
85
+
86
+ def review_goals(self, spec: Any, *,
87
+ context: Optional[Mapping[str, Any]] = None) -> CoordinationReportLike:
88
+ """:meth:`review` for a built ``StudioMovieSpec`` (submit preflight)."""
89
+ ...
90
+
91
+ def apply_decisions(self, rows: Sequence[Mapping[str, Any]],
92
+ report: CoordinationReportLike,
93
+ ) -> Tuple[List[Dict[str, Any]], List[KnobDecisionLike]]:
94
+ """Apply the report's ``set`` decisions to copies of ``rows``; return
95
+ ``(rows, applied)``. May write mechanics only, never ``prompt``."""
96
+ ...
97
+
98
+ def blocking_mismatches(self, report: CoordinationReportLike,
99
+ threshold: Optional[float] = None) -> List[Dict[str, Any]]:
100
+ """Submit-blocking rows in ``movie_plan.preflight_movie``'s vocabulary
101
+ (``index`` / ``segment_id`` / ``reason`` / ``detail``)."""
102
+ ...
103
+
104
+
105
+ class NullCoordinationReport:
106
+ """The honest empty report: nothing reviewed, nothing decided."""
107
+
108
+ decisions: Tuple[Any, ...] = ()
109
+ segments: Tuple[Any, ...] = ()
110
+
111
+ def as_dict(self) -> Dict[str, Any]:
112
+ return {"reviewed": False, "coordinator": "none",
113
+ "segments": [], "decisions": [], "expectations": []}
114
+
115
+
116
+ class NullPromptCoordinator:
117
+ """Default: no review, no decisions, nothing blocks (single-box / tests)."""
118
+
119
+ block_confidence = 0.8
120
+
121
+ def review(self, rows, *, notes: str = "", context=None, llm=None):
122
+ return NullCoordinationReport()
123
+
124
+ def review_goals(self, spec, *, context=None):
125
+ return NullCoordinationReport()
126
+
127
+ def apply_decisions(self, rows, report):
128
+ return [dict(r) for r in rows], []
129
+
130
+ def blocking_mismatches(self, report, threshold=None):
131
+ return []
132
+
133
+
134
+ _providers: Dict[str, Any] = {}
135
+ _defaults: Dict[str, Any] = {"prompt_coordinator": NullPromptCoordinator()}
136
+
137
+
138
+ def get_prompt_coordinator() -> PromptCoordinator:
139
+ return _providers.get("prompt_coordinator", _defaults["prompt_coordinator"])
140
+
141
+
142
+ def set_prompt_coordinator(impl: PromptCoordinator | None) -> None:
143
+ """Install (or, with ``None``, uninstall) the coordinator implementation."""
144
+ if impl is None:
145
+ _providers.pop("prompt_coordinator", None)
146
+ else:
147
+ _providers["prompt_coordinator"] = impl
148
+
149
+
150
+ def reset_hooks() -> None:
151
+ """Drop every installed hook (tests)."""
152
+ _providers.clear()
@@ -0,0 +1,10 @@
1
+ """hugpy Video Intelligence — headless backbone (Phases 1 & 2).
2
+
3
+ Additive package. Nothing here restarts a service or touches the serve/worker
4
+ control plane. It provides the frozen schemas (MediaRef / CropSpec / JobSpec /
5
+ JobResult), the ingest metadata resolver, the pure ffmpeg crop runner, and a
6
+ durable sqlite-backed job bus.
7
+
8
+ Build order matches hugpy_video_intelligence_map.md §11:
9
+ MediaRef + ingest -> CropSpec + (ffmpeg, crop) runner + crop JobSpec -> bus.
10
+ """
@@ -0,0 +1,44 @@
1
+ """Audio-extraction schema — map §4.4.
2
+
3
+ Frozen spec + validating factory, mirroring frame_schema.py exactly. An audio
4
+ extraction pulls a video's audio track out to a standalone audio file of one
5
+ `fmt`. Per-field validity (`source` is a video, `fmt` is a known container) is
6
+ enforced in the factory with LOCAL raises (construction-time, never across a
7
+ boundary). Temporal trimming of the resulting audio is NOT this job's concern —
8
+ that already works through the existing `crop` job's temporal branch.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Literal
14
+
15
+ from hugpy_video.intel.media_schema import MediaRef
16
+
17
+ AudioFmt = Literal["wav", "mp3", "m4a"]
18
+
19
+ # Construction-time guard set for `fmt` (registry-over-globals discipline).
20
+ AUDIO_FMTS = frozenset({"wav", "mp3", "m4a"})
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class AudioExtractSpec:
25
+ """Extract `source`'s (a video) audio track into a standalone audio file.
26
+
27
+ fmt output container/codec family: wav|mp3|m4a (default wav)
28
+ """
29
+ source: MediaRef
30
+ fmt: str = "wav"
31
+
32
+
33
+ def make_audio_extract(source: MediaRef, fmt: str = "wav") -> AudioExtractSpec:
34
+ """Validate + build an AudioExtractSpec. A raise here is fine: local to
35
+ construction, never crosses a boundary (map §4 / §6).
36
+
37
+ Also the reconstruction path used by the bus when it deserializes the spec
38
+ from JSON — kwargs line up 1:1 with the fields so it round-trips.
39
+ """
40
+ if source.kind != "video":
41
+ raise ValueError(f"audio_extract source must be a video; got kind={source.kind!r}")
42
+ if fmt not in AUDIO_FMTS:
43
+ raise ValueError(f"fmt must be one of {sorted(AUDIO_FMTS)}; got {fmt!r}")
44
+ return AudioExtractSpec(source=source, fmt=fmt)