Metadata-Version: 2.4
Name: hugpy-video
Version: 0.3.6
Summary: Hugpy video: media library and job bus, ffmpeg, synthetic and studio runners, reservations and studio sessions for video intelligence
Author-email: putkoff <support@hugpy.ai>
License-Expression: LicenseRef-Proprietary
Project-URL: Homepage, https://hugpy.ai
Project-URL: Documentation, https://github.com/hugpy/hugpy/blob/main/py/cinema/hugpy_video/README.md
Project-URL: Repository, https://github.com/hugpy/hugpy
Project-URL: Source, https://github.com/hugpy/hugpy/tree/main/py/cinema/hugpy_video
Project-URL: Issues, https://github.com/hugpy/hugpy/issues
Project-URL: Changelog, https://github.com/hugpy/hugpy/releases
Project-URL: Architecture, https://github.com/hugpy/hugpy/blob/main/PARTITION.md
Keywords: hugpy,llm,self-hosted,video,ffmpeg,video-generation,media-intelligence
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: flask>=2
Requires-Dist: pydantic>=2
Requires-Dist: abstract-essentials
Requires-Dist: requests>=2.28
Requires-Dist: abstract-flask>=0.0.0.1052
Requires-Dist: hugpy-platform>=0.3.0
Requires-Dist: hugpy-media>=0.2.8
Requires-Dist: hugpy-engine>=0.3.2
Requires-Dist: hugpy-oracle<0.4,>=0.3.11
Provides-Extra: render
Requires-Dist: numpy; extra == "render"
Requires-Dist: Pillow; extra == "render"
Requires-Dist: requests; extra == "render"
Provides-Extra: studio
Requires-Dist: torch; extra == "studio"
Requires-Dist: diffusers; extra == "studio"
Requires-Dist: transformers; extra == "studio"
Requires-Dist: numpy; extra == "studio"
Requires-Dist: Pillow; extra == "studio"
Requires-Dist: opencv-python-headless; extra == "studio"
Requires-Dist: imageio; extra == "studio"
Requires-Dist: imageio-ffmpeg; extra == "studio"
Requires-Dist: requests; extra == "studio"
Provides-Extra: identity
Requires-Dist: abstract-identity; extra == "identity"
Requires-Dist: requests; extra == "identity"
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: pytest-timeout; extra == "test"
Dynamic: license-file

# hugpy-video

The **video arm** of Hugpy (import name `hugpy_video`). The source tree is at
**0.3.5**; the installed deb and running service on this host are **0.3.4**.
It covers execution and artifacts for video work:

* the media library jail and the sqlite **media job bus**, with its local
  runners (ffmpeg crop, frames and audio);
* engine-backed generation (image, scene and movie), the studio spine (t2v/i2v
  clips, segmented movies, the model battery) and the GPU reservation engine;
* the identity-profile library with relays to the identity render service;
* LLM prompt assist;
* the `/video` Video Intelligence console (SPA) and its toolset API
  (`/api/video/*`, `/api/movie/*`), served by its own service on
  `127.0.0.1:7341`. Hugpy Core proxies both.

The arm contract (toolset → Blueprint → standalone server → `arms.d` → core
proxy) is in [docs/arms.md](../../docs/arms.md). How engine-backed routes choose
and fall back between models is in
[docs/execute_prompt.md](../../docs/execute_prompt.md).

## Dependency on hugpy-oracle

**`hugpy-oracle` is a hard dependency** (`hugpy-oracle>=0.3.11,<0.4` in 0.3.5;
`>=0.3.10` in 0.3.4; operator ruling 2026-10-15). The dependency graph runs
**video → oracle**. The oracle's use of video is lazy and optional: its `video` extra pulls in `hugpy-video`, and
it reaches video's studio registry, runners and media bus only inside the
functions that need them. Video imports the oracle **lazily**, inside the
toolset callables that need it (`studio/plan`, `producer/plan`,
`performance/probe`, `script/runs`, `script/sources`), so `import hugpy_video`
loads no planner. The oracle's planners install into video's hooks
(`hugpy_video.bus.hooks`) through `hugpy_oracle.install_hooks()`. That call is
made idempotently by the planner routes, and by the `hugpy.video.hooks` entry
point when `hugpy_video.web.api` loads.

## Layout

```
src/hugpy_video/
  imports/        the shared layer: imports no optional or heavy backend
    constants.py  ARM_NAME="video", UI_PREFIX="/video/", every env-var NAME and default
    functions.py  env_value()/env_bool(), parse_env_file(), lazy()/first_lazy()
    classes.py    VideoError, ToolError (the toolset error, which becomes {"error": ...}),
                  JobError (the job error-as-data, lifted from hugpy_control in 0.3.5)
    store.py      stdlib cross-process sqlite helpers, lifted from hugpy_control in 0.3.5
                  (used by the studio-assist log and the reservation ledger; same env
                  names and paths, so existing stores keep working)
    init_imports.py, __init__.py
  toolset.py      get_toolset() -> {"video": {...}, "movie": {...}}, get_manifest()
  routes.py       video_bp = register_categories(get_bp("video"), get_toolset())
                  plus the native SSE escape hatches
  serve/          the standalone service: `hugpy-video serve` (app.py: create_app, serve)
  ui.py           dist_dir() -> ui_dist/ (built @hugpy/video-intelligence-ui)
  web/            the Flask view implementation the toolset adapts: api (read surface,
                  presets, identity library, media/clip bytes), generate, identity_write,
                  movie_routes, assist, studio_planning, render_catalog, jobstream (SSE),
                  studio_assist_log, video_intent
  bus/            the job-bus control plane: jobs (public API, register_job), job_events
                  (worker -> core transitions), hooks (PromptCoordinator and planner seams)
  intel/          media_store (refs and jail), media_bus (sqlite bus), job_bridge,
                  job_schema, presets, reservation/, runners/ (ffmpeg, diffusers plane,
                  studio, identity relays, MLT), studio/ (the studio spine), plane.py
                  (the one seam onto hugpy_engine.execute_prompt)
  chat_video/     chat-side frame analysis over a hugpy_media vision runner
  video_gen/      StudioVideoRunner / VideoGenRequest (engine runner for t2v/i2v)
  plugin.py       hugpy_engine.tasks entry point: text-to-video, image-to-video
  state.py        injectable state roots;  config.py  HUGPY_API_KEY / HUGPY_BASE loader
  cli.py          `hugpy-video`;  selftest.py;  sql/001_video_jobs_notify.sql
  _lifted/        helpers lifted from the retired monolith
```

The old top-level module paths (`hugpy_video.api`, `.jobstream`, `.jobs`,
`.hooks`, `.render_catalog`, `.studio_assist_log`, ...) still import. They are
transparent `sys.modules` aliases to the new locations. Wildcard imports are
forbidden (`tests/test_import_policy.py`).

## The toolset (`video/*` 56 routes, `movie/*` 2 routes)

Each route is `GET`+`POST` at `/api/<category>/<route>`, with a trailing-slash
variant. Ids are kwargs (`?job_id=`, `?slug=`, `?preset_id=`, `?movie_id=`), not
path segments. The return value comes back as `{"result": ...}` and an error as
`500 {"error": "<real message>"}`. Byte routes return the file `Response`
directly (status 200, 206 or 404 preserved). Source: `toolset.get_toolset()`,
verified against the live `GET /api/video/manifest`.

### Works standalone (no core, no GPU)

| Route | Notes |
|---|---|
| `health`, `manifest` | identity; the manifest includes `escape_hatches` |
| `jobs` | job bus listing; reads `?all`, `?stale`, `?limit`, `?owner` |
| `job`, `job/cancel`, `job/private` | `job_id`; `job/private` reads `private` from the body |
| `media` | `handle`: raw bytes for a jailed MediaRef (Range supported) |
| `projects`, `presets` | catalog reads |
| `studio/clips`, `studio/clip`, `studio/clip/detail`, `studio/clip/archive`, `studio/clip/unarchive` | `job_id`; `studio/clip` streams the mp4. Archive never deletes. |
| `studio/presets`, `studio/presets/apply` | `preset_id` |
| `identity-profiles`, `identity-profile`, `identity-profile/delete`, `identity-profile/private` | `slug`; delete archives, never hard-deletes |
| `ingest` | turns a path under the video storage root into a MediaRef |
| `jobs/crop`, `jobs/frame_extract`, `jobs/audio_extract` | ffmpeg runners on the local bus (needs `ffmpeg`; the deb depends on it) |
| `prompt/assist/log`, `prompt/assist/ticket` (`ticket`), `studio/movies` | local reads |
| `performance/probe` | the oracle's `(oracle, performance)` readiness, reported as data: `ready`, `bound`, `unbound` with the operator step for each gap |
| `script/sources`, `script/runs` | the oracle's script-first run store. `GET script/runs` lists runs; `POST` starts one. |
| `movie/presets`, `movie/presets/apply` | `preset_id` |

### Needs a reachable core and GPU fleet

Each of these first checks `HUGPY_CORE_URL` (default `http://127.0.0.1:7020`,
1.5 s). If nothing answers, the route raises `ToolError` naming the variable and
`/etc/hugpy/video.env`, and does not enqueue anything. The check proves only
that something answers HTTP at that URL; the fleet itself is reached later,
through `execute_prompt`.

`jobs/generate_image`, `jobs/generate_scene`, `jobs/generate_movie`,
`studio/i2v`, `studio/tester`, `studio/movie`, `studio/movie/pause`
(`movie_id`), `studio/movie/resume` (`movie_id`), `studio/plan` and
`producer/plan` (oracle-backed planners), `prompt/assist`, `prompt/intent`,
`prompt/assist/models`, `render/presets`.

### Needs the identity render service

These require `IDENTITY_RENDER_URL` (and `IDENTITY_RENDER_TOKEN`). Without it
they raise a `ToolError` that names the variables:

`identity-profiles/create`, `identity-profiles/from-video`,
`identity-profiles/from-images`, `identity-profiles/video-extract`,
`identity-profile/reconstruction` (`slug`), `identity-profile/canonical`
(`slug`), `identity-profile/generate` (`slug`). The `identity` extra installs
the `abstract-identity` client. It does not start a GPU service.

### Stubs: reachable, not implemented in this build

These always answer `500 {"error": "<route> is not implemented in this
hugpy-video build: <what it needs>"}`. They never return 404:

| Route | Missing piece |
|---|---|
| `identity-render/probe` | the identity render service probe |
| `studio/clip/to-editor` | the Filmora editor handoff (ffprobe/remux) |
| `identity-profile/settings` | the identity `gen_settings` write (not ported) |
| `identity-profile/update` | the identity profile name/notes write (not ported) |
| `identity-profile/versions` | identity version management (not ported) |

### Escape hatches (native Blueprint routes)

| Route | Kind |
|---|---|
| `GET /api/video/jobs/stream` | SSE. Sends a `stream.ready` marker, then one row per job transition, filtered to what the caller may see. Capped at one hour; the client re-seeds from `video/jobs`. |
| `GET /api/video/prompt/assist/log/stream` | SSE: the studio assist log |
| `POST /api/video/jobs/event` | internal ingest: a worker reports `{job_id, status}`. Returns 202. Not listed in `escape_hatches`. |

**The job stream is single-process by default.** With
`HUGPY_VIDEO_JOBS_PG_DSN` unset, a reported transition goes straight into this
process's in-memory event ring and no Postgres listener starts. Set it (for
example `dbname=hugpy_video`) only when several processes share one bus and
need Postgres `LISTEN/NOTIFY` to fan events out. That needs
`sql/001_video_jobs_notify.sql` and `psycopg`. If Postgres is then unreachable,
the listener retries and the stream degrades to the initial seed.

### Not served (pending)

* **`POST /video/jobs/performance`.** The Oracle station's performance submit
  is not in the toolset, and the standalone service returns 404. The legacy
  `web/api.py` registers it only as a "501 not available" placeholder. Use
  `performance/probe` to see which seams are unbound.
* **Video UI uploads.** The SPA's file stations (image crop, audio crop, frame
  extract, identity upload, studio generate) upload to `/api/uploads`. Neither
  this service nor core 0.4.40 serves that route (404).

## Standalone service

```bash
hugpy-video serve [--host H] [--port P] [--env-file /etc/hugpy/video.env]
```

`create_app()` does the following:

* loads the env file without overriding the environment;
* creates the state directories and initialises the sqlite job bus;
* builds `abstract_flask.get_Flask_app(name="hugpy-video",
  routes=hugpy_video.routes, url_prefix="api")`;
* serves the SPA at `/video/` with history fallback. `/` and `/video` redirect
  to `/video/`. A build without `ui_dist` serves only the API.
* forwards `/v1/*` (streamed) to `HUGPY_CORE_URL`. This exists because the
  UI's model pickers read core's `/v1/models` and `/v1/task-bindings`. With core
  unreachable it answers `503 {"error": "hugpy core unreachable at ..."}`.

Then it runs Flask threaded, so SSE does not block other requests.

**Demo media.** The served `index.html` marker
`window.__HUGPY_MEDIA_BASE__ = window.__HUGPY_MEDIA_BASE__ ?? null;` is
rewritten from the environment. `HUGPY_DEMO_MEDIA_DIR` (served by this process
at `/demo-media/`) takes precedence over `HUGPY_DEMO_MEDIA_BASE` (a URL).

```bash
curl -s 127.0.0.1:7341/api/video/health      # {"result":{"arm":"video","ok":true,"version":"0.3.4"}}
curl -s '127.0.0.1:7341/api/video/jobs?limit=5'
curl -s 127.0.0.1:7341/api/video/performance/probe
curl -sN 127.0.0.1:7341/api/video/jobs/stream
```

## CLI

| Command | What it does |
|---|---|
| `hugpy-video serve [...]` | the standalone service |
| `hugpy-video jobs list [--all] [--limit N] [--json]` | the media job bus |
| `hugpy-video jobs registry` | registered job names, runner keys, queues and timeouts |
| `hugpy-video selftest` | in-process checks (no GPU, no service); exits 1 on failure |
| `hugpy-video state` | prints the resolved state roots |
| `hugpy-video models audit [--json]` | declared studio models: runner gaps, weight pin, VRAM by precision. Loads no weights. |
| `hugpy-video --version` | |

## Environment

The names are defined in `imports/constants.py`.

| Variable | Default | Effect |
|---|---|---|
| `HUGPY_VIDEO_HOST` / `HUGPY_VIDEO_PORT` | `127.0.0.1` / `7341` | bind address of `serve` |
| `HUGPY_VIDEO_STATE_DIR` | `<DEFAULT_ROOT>/video_intel` (the deb uses `/var/lib/hugpy-video`) | job bus and studio sessions |
| `HUGPY_MEDIA_JOBS_DB` | `<state>/media_jobs.db` | sqlite job bus |
| `HUGPY_RESERVATIONS_DB` | `<PROJECTS_HOME>/reservations.db` | GPU reservation ledger |
| `STUDIO_OUTPUT_ROOT` | `<state>/studio` | studio sessions and renders |
| `DEFAULT_ROOT`, `UPLOADS_HOME`, `IDENTITIES_HOME`, `PROJECTS_HOME` | `hugpy_platform` (the deb sets `DEFAULT_ROOT=/var/lib/hugpy-video/root`) | platform storage roots; the media jail is `UPLOADS_HOME` plus `DEFAULT_ROOT` |
| `HUGPY_CORE_URL` | `http://127.0.0.1:7020` | engine preflight, `/v1` forwarder, engine relays |
| `HUGPY_VIDEO_JOBS_PG_DSN` | unset (single-process) | multi-process job-stream fan-out |
| `IDENTITY_RENDER_URL`, `IDENTITY_RENDER_TOKEN` | unset | identity render service |
| `HUGPY_DEMO_MEDIA_DIR`, `HUGPY_DEMO_MEDIA_BASE` | unset | demo-media marker rewrite |
| `HUGPY_WORKER_NAME` | unset | worker identity on reported job events |
| `HUGPY_API_KEY`, `HUGPY_BASE` | `""`, `https://dev.hugpy.ai/api` | `config.load_config()` credentials. The legacy aliases `CONSOLE_API`, `HUGPY_API_TOKEN`, `HUGPY_AGENT_API`, `HUGPY_API_KEY_NEWVM` / `HUGPY_BASE_URL`, `HUGPY_URL`, `HUGPY_CENTRAL` are accepted and recorded in `Config.deprecations`. |

Tuning knobs read by the runners include `HUGPY_MEDIA_BUS_*` (runners,
lookahead, stale and reap timing), `HUGPY_RESERVATION*`, `STUDIO_WEIGHTS_ROOT`,
`STUDIO_MAX_VRAM_GB`, `STUDIO_ALLOW_UNPINNED` and `HUGPY_SCENE_CONCURRENCY`.
Their defaults live next to the code that reads them.

## Debian package

```bash
BUILD_PYTHON=/usr/bin/python3 packaging/debian/build.sh     # -> build/debian/hugpy-video_<ver>_amd64.deb (Python 3.12)
sudo apt install ./build/debian/hugpy-video_<ver>_amd64.deb      # 0.3.4 is the newest deb built so far
```

`build.sh` bundles offline wheels for `hugpy-video` and its local siblings
(`hugpy-oracle`, `hugpy-platform`, `hugpy-media`, `hugpy-engine`) and their
public dependencies. The 0.3.4 deb also bundled `hugpy-control` and
`hugpy-storage`. Neither is needed since video 0.3.5, oracle 0.3.11 and
media 0.3.4. `build.sh` asserts that the wheel contains `ui_dist/index.html`
and `sql/001_video_jobs_notify.sql`.
The control file declares `Depends: python3 (>= 3.12), python3 (<< 3.13),
python3-venv, systemd, ffmpeg`.

`postinst`:

* builds `/opt/hugpy-video/venv` offline and force-reinstalls the bundled
  siblings;
* creates the `hugpy-video` system user, a member of the `hugpy` group;
* creates `/var/lib/hugpy-video` (mode 0750) with `root/` inside it;
* writes `/etc/hugpy/video.env` if it is absent;
* writes `hugpy-video.service` (`WorkingDirectory=/var/lib/hugpy-video`,
  `ProtectSystem=strict`, `ReadWritePaths` set to the state directory);
* writes `/usr/share/hugpy/arms.d/video.json` (categories `video` and `movie`,
  UI `/video/`; the upstream port follows `video.env`);
* restarts the service.

Core then proxies `/api/video/*`, `/api/movie/*` and `/video/*`. `postrm`
removes the unit and the `arms.d` file. On purge it also removes the venv, the
state, `video.env` and the user. Host-level steps are in
[docs/install.md](../../docs/install.md).

## Dependencies

Runtime (`pyproject.toml`, 0.3.5): `flask>=2`, `pydantic>=2`,
`abstract-essentials`, `requests>=2.28`, `abstract-flask>=0.0.0.1052`,
`hugpy-platform>=0.3.0`, `hugpy-media>=0.2.8`, `hugpy-engine>=0.3.2`,
`hugpy-oracle>=0.3.11,<0.4`.

`hugpy-control` is no longer a dependency as of 0.3.5. Its sqlite store helpers
and `JobError` now live in `hugpy_video.imports` (`store.py`, `classes.py`).
Version 0.3.4 depended on `hugpy-control>=0.3.0`.

Extras: `render` (numpy, Pillow, requests), `studio` (torch, diffusers,
transformers and the rest of the GPU set), `identity` (abstract-identity),
`test`. GPU and runtime pieces are never imported at module scope.

## Entry points

| Group | Name → target |
|---|---|
| `console_scripts` | `hugpy-video` → `hugpy_video.cli:main` |
| `hugpy.arms` | `video` → `hugpy_video.routes:video_bp` |
| `hugpy.toolsets` | `video` → `hugpy_video.toolset:get_toolset` |
| `hugpy_engine.tasks` | `video` → `hugpy_video.plugin:register` (`text-to-video`, `image-to-video`) |

## Rules

* Heavy imports are lazy. The package must import on a CPU-only box.
* The version is single-sourced from `pyproject.toml` through
  `importlib.metadata`.
* Credentials come only through `hugpy_video.config.load_config`.

## UI

The SPA source is [`ui/video_intelligence_ui`](../../ui/video_intelligence_ui/README.md).
Its build is copied into `src/hugpy_video/ui_dist/`, and the provenance is in
`ui_dist/PROVENANCE.md`.
