anchorcast 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.
- anchorcast-0.1.0/.github/workflows/ci.yml +28 -0
- anchorcast-0.1.0/.github/workflows/publish.yml +31 -0
- anchorcast-0.1.0/.gitignore +11 -0
- anchorcast-0.1.0/AGENTS.md +44 -0
- anchorcast-0.1.0/CHANGELOG.md +23 -0
- anchorcast-0.1.0/CODE_OF_CONDUCT.md +62 -0
- anchorcast-0.1.0/CONTRIBUTING.md +43 -0
- anchorcast-0.1.0/LICENSE +21 -0
- anchorcast-0.1.0/PKG-INFO +177 -0
- anchorcast-0.1.0/README.md +150 -0
- anchorcast-0.1.0/SECURITY.md +17 -0
- anchorcast-0.1.0/examples/__init__.py +1 -0
- anchorcast-0.1.0/examples/characters/literary-bear/CREDIT.txt +3 -0
- anchorcast-0.1.0/examples/characters/literary-bear/pooh-1926.png +0 -0
- anchorcast-0.1.0/examples/feed_session.py +38 -0
- anchorcast-0.1.0/examples/literary_bear/__init__.py +1 -0
- anchorcast-0.1.0/examples/literary_bear/__main__.py +6 -0
- anchorcast-0.1.0/examples/literary_bear/app.py +209 -0
- anchorcast-0.1.0/examples/literary_bear/player.html +192 -0
- anchorcast-0.1.0/examples/literary_bear/scripts.py +119 -0
- anchorcast-0.1.0/examples/literary_bear/source.py +44 -0
- anchorcast-0.1.0/pyproject.toml +40 -0
- anchorcast-0.1.0/src/anchorcast/__init__.py +32 -0
- anchorcast-0.1.0/src/anchorcast/continuity.py +19 -0
- anchorcast-0.1.0/src/anchorcast/media.py +48 -0
- anchorcast-0.1.0/src/anchorcast/models/__init__.py +5 -0
- anchorcast-0.1.0/src/anchorcast/models/base.py +5 -0
- anchorcast-0.1.0/src/anchorcast/models/h3_max.py +61 -0
- anchorcast-0.1.0/src/anchorcast/py.typed +0 -0
- anchorcast-0.1.0/src/anchorcast/session.py +98 -0
- anchorcast-0.1.0/src/anchorcast/sources/__init__.py +7 -0
- anchorcast-0.1.0/src/anchorcast/sources/feed.py +39 -0
- anchorcast-0.1.0/src/anchorcast/sources/idle.py +18 -0
- anchorcast-0.1.0/src/anchorcast/sources/queue.py +21 -0
- anchorcast-0.1.0/src/anchorcast/types.py +84 -0
- anchorcast-0.1.0/tests/test_character.py +23 -0
- anchorcast-0.1.0/tests/test_continuation_prompt.py +39 -0
- anchorcast-0.1.0/tests/test_feed_source.py +29 -0
- anchorcast-0.1.0/tests/test_idle_source.py +19 -0
- anchorcast-0.1.0/tests/test_literary_scripts.py +21 -0
- anchorcast-0.1.0/tests/test_queue_source.py +18 -0
- anchorcast-0.1.0/tests/test_session.py +76 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
- name: Install
|
|
24
|
+
run: python -m pip install -e ".[dev]"
|
|
25
|
+
- name: Ruff
|
|
26
|
+
run: python -m ruff check src tests
|
|
27
|
+
- name: Pytest
|
|
28
|
+
run: python -m pytest
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
environment: pypi
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write
|
|
16
|
+
contents: read
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.11"
|
|
22
|
+
- name: Tag matches package version
|
|
23
|
+
run: |
|
|
24
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
25
|
+
version="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")"
|
|
26
|
+
test "$tag" = "$version"
|
|
27
|
+
- name: Build
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install build
|
|
30
|
+
python -m build
|
|
31
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Agent notes
|
|
2
|
+
|
|
3
|
+
This repository is a **Python client** for generative livestreams. It is not a YouTube app, not a character, and not a news show.
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
- `src/anchorcast/` — library
|
|
8
|
+
- `tests/` — pytest, no fal network calls
|
|
9
|
+
- `examples/` — apps that consume the library, including a literary-bear news stream
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
Keep these boundaries:
|
|
14
|
+
|
|
15
|
+
| Layer | Owns |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `Brief` | What to say next (topic, talking points, citations) |
|
|
18
|
+
| `Model` | `generate(request) -> Segment` |
|
|
19
|
+
| `Source` | `next_brief(playhead) -> Brief \| None` |
|
|
20
|
+
| `Continuity` | Which still starts the next clip |
|
|
21
|
+
| `Session` | Generate-ahead buffer and events |
|
|
22
|
+
|
|
23
|
+
Do **not** add YouTube, Twitch, TikTok, Stripe, or Super Chat integrations here. Those belong in a later backend that maps platform events into `Brief`s on a `QueueSource`.
|
|
24
|
+
|
|
25
|
+
Do **not** let `IdleSource` (or any default source) invent news claims. Idle is character bits only.
|
|
26
|
+
|
|
27
|
+
## Python
|
|
28
|
+
|
|
29
|
+
- 3.11+
|
|
30
|
+
- Double quotes
|
|
31
|
+
- Type annotations on public functions and dataclasses
|
|
32
|
+
- `gpt-5.6-luna` for OpenAI calls in examples (cheap GPT-5.6 tier)
|
|
33
|
+
|
|
34
|
+
## Checks
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python3.11 -m pip install -e ".[dev]"
|
|
38
|
+
python3.11 -m pytest
|
|
39
|
+
python3.11 -m ruff check src tests
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
New behavior needs a failing test first unless it is config, examples, or generated files.
|
|
43
|
+
|
|
44
|
+
The H3 Max adapter hits fal. Do not call it from unit tests. Use a fake `Model`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] — 2026-09-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `Session` with a generate-ahead buffer
|
|
13
|
+
- `Brief`, `Character`, `Segment`, and `StreamEvent` types
|
|
14
|
+
- `H3Max` model adapter (`minimax/h3-max/image-to-video`)
|
|
15
|
+
- `QueueSource`, `FeedSource`, and `IdleSource`
|
|
16
|
+
- `LastFrameContinuity` and `IdentityContinuity`
|
|
17
|
+
- MIT license
|
|
18
|
+
- GitHub Actions CI (pytest, ruff) and tag-triggered PyPI publish
|
|
19
|
+
- `Session.pump()` for live apps
|
|
20
|
+
- Follow-up clips prompt the model to continue the take
|
|
21
|
+
- `examples/literary_bear` continuous news stream with continuing spoken lines
|
|
22
|
+
|
|
23
|
+
`Brief` has no paid/unpaid taxonomy. Use `priority` to order the queue.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our
|
|
6
|
+
community a harassment-free experience for everyone, regardless of age, body
|
|
7
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
8
|
+
identity and expression, level of experience, education, socio-economic status,
|
|
9
|
+
nationality, personal appearance, race, caste, color, religion, or sexual
|
|
10
|
+
identity and orientation.
|
|
11
|
+
|
|
12
|
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
13
|
+
diverse, inclusive, and healthy community.
|
|
14
|
+
|
|
15
|
+
## Our Standards
|
|
16
|
+
|
|
17
|
+
Examples of behavior that contributes to a positive environment:
|
|
18
|
+
|
|
19
|
+
- Demonstrating empathy and kindness toward other people
|
|
20
|
+
- Being respectful of differing opinions, viewpoints, and experiences
|
|
21
|
+
- Giving and gracefully accepting constructive feedback
|
|
22
|
+
- Accepting responsibility and apologizing to those affected by our mistakes,
|
|
23
|
+
and learning from the experience
|
|
24
|
+
- Focusing on what is best not just for us as individuals, but for the overall
|
|
25
|
+
community
|
|
26
|
+
|
|
27
|
+
Examples of unacceptable behavior:
|
|
28
|
+
|
|
29
|
+
- The use of sexualized language or imagery, and sexual attention or advances of
|
|
30
|
+
any kind
|
|
31
|
+
- Trolling, insulting or derogatory comments, and personal or political attacks
|
|
32
|
+
- Public or private harassment
|
|
33
|
+
- Publishing others' private information, such as a physical or email address,
|
|
34
|
+
without their explicit permission
|
|
35
|
+
- Other conduct which could reasonably be considered inappropriate in a
|
|
36
|
+
professional setting
|
|
37
|
+
|
|
38
|
+
## Enforcement Responsibilities
|
|
39
|
+
|
|
40
|
+
Project maintainers are responsible for clarifying and enforcing our standards of
|
|
41
|
+
acceptable behavior and will take appropriate and fair corrective action in
|
|
42
|
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
43
|
+
or harmful.
|
|
44
|
+
|
|
45
|
+
## Scope
|
|
46
|
+
|
|
47
|
+
This Code of Conduct applies within all community spaces, and also applies when
|
|
48
|
+
an individual is officially representing the community in public spaces.
|
|
49
|
+
|
|
50
|
+
## Enforcement
|
|
51
|
+
|
|
52
|
+
Report incidents by opening a private GitHub security advisory on this
|
|
53
|
+
repository or by contacting the maintainer through GitHub. All complaints will
|
|
54
|
+
be reviewed and investigated promptly and fairly.
|
|
55
|
+
|
|
56
|
+
## Attribution
|
|
57
|
+
|
|
58
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
59
|
+
version 2.1, available at
|
|
60
|
+
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
|
|
61
|
+
|
|
62
|
+
[homepage]: https://www.contributor-covenant.org
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for looking at Anchorcast. The useful work is the `Session` / `Model` / `Source` / `Brief` interface. Keep that small.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python3.11 -m pip install -e ".[dev]"
|
|
9
|
+
python3.11 -m pytest
|
|
10
|
+
python3.11 -m ruff check src tests
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Python 3.11+ and ffmpeg on `PATH`. Unit tests must not call fal.
|
|
14
|
+
|
|
15
|
+
## Pull requests
|
|
16
|
+
|
|
17
|
+
- One idea per PR
|
|
18
|
+
- Tests for behavior changes
|
|
19
|
+
- Do not land YouTube / Twitch / TikTok / payments in this repo
|
|
20
|
+
- Match existing style: double quotes, type hints
|
|
21
|
+
|
|
22
|
+
Coding agents should read [AGENTS.md](AGENTS.md).
|
|
23
|
+
|
|
24
|
+
## Releasing
|
|
25
|
+
|
|
26
|
+
CI runs pytest and ruff on every push and pull request.
|
|
27
|
+
|
|
28
|
+
PyPI publishes from a version tag via [trusted publishing](https://docs.pypi.org/trusted-publishers/). No API token in GitHub secrets.
|
|
29
|
+
|
|
30
|
+
1. Bump `version` in `pyproject.toml` and `src/anchorcast/__init__.py`, and add a `CHANGELOG.md` entry.
|
|
31
|
+
2. On PyPI: Publishing → add a trusted publisher (`scrollmark/anchorcast`, workflow `publish.yml`, environment `pypi`). Needed once, before the first release.
|
|
32
|
+
3. Tag and push:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git tag v0.1.0
|
|
36
|
+
git push origin v0.1.0
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The tag must match the package version (`v0.1.0` for `0.1.0`). The `pypi` GitHub Environment must exist on the repo.
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
Contributions are MIT, same as the rest of the project.
|
anchorcast-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nikhil Sharma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: anchorcast
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python client for real-time media streams generated by pluggable models.
|
|
5
|
+
Project-URL: Homepage, https://github.com/scrollmark/anchorcast
|
|
6
|
+
Project-URL: Repository, https://github.com/scrollmark/anchorcast
|
|
7
|
+
Project-URL: Issues, https://github.com/scrollmark/anchorcast/issues
|
|
8
|
+
Author: Nikhil Sharma
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: fal,generative-video,h3-max,livestream
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Multimedia :: Video
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: fal-client>=0.5.0
|
|
22
|
+
Requires-Dist: feedparser>=6.0.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Anchorcast
|
|
29
|
+
|
|
30
|
+
A Python client for **real-time media streams generated by pluggable models**.
|
|
31
|
+
|
|
32
|
+
[](https://github.com/scrollmark/anchorcast/actions/workflows/ci.yml)
|
|
33
|
+
[](https://pypi.org/project/anchorcast/)
|
|
34
|
+
[](LICENSE)
|
|
35
|
+
[](https://www.python.org/downloads/)
|
|
36
|
+
|
|
37
|
+
> Early (`0.1.0`). The session / model / source interface is the point. Expect it to move.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install anchorcast
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
From git:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install git+https://github.com/scrollmark/anchorcast.git
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For local development:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/scrollmark/anchorcast.git
|
|
55
|
+
cd anchorcast
|
|
56
|
+
python3.11 -m pip install -e ".[dev]"
|
|
57
|
+
python3.11 -m pytest
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Requirements:** Python 3.11+, [ffmpeg](https://ffmpeg.org/) on `PATH` (for last-frame continuity), and a [fal](https://fal.ai) key if you use the H3 Max adapter (`FAL_KEY` or `FALAI_API_KEY`).
|
|
61
|
+
|
|
62
|
+
## Quickstart
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
|
|
67
|
+
from anchorcast import Brief, Character, QueueSource, Session
|
|
68
|
+
from anchorcast.models import H3Max
|
|
69
|
+
|
|
70
|
+
source = QueueSource()
|
|
71
|
+
session = Session(
|
|
72
|
+
model=H3Max(),
|
|
73
|
+
character=Character(
|
|
74
|
+
name="Bear",
|
|
75
|
+
image=Path("bear.png"),
|
|
76
|
+
visual_prompt="Hand-drawn stuffed bear on cream paper.",
|
|
77
|
+
voice_prompt="warm, unhurried cadence",
|
|
78
|
+
),
|
|
79
|
+
source=source,
|
|
80
|
+
workdir=Path(".anchorcast"),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
source.submit(
|
|
84
|
+
Brief(
|
|
85
|
+
id="rates",
|
|
86
|
+
topic="mortgage rates",
|
|
87
|
+
talking_points=("Why are mortgage rates still so high?",),
|
|
88
|
+
citations=("https://example.com/rates",),
|
|
89
|
+
priority=10,
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
for event in session.run(max_segments=1):
|
|
94
|
+
print(event.segment.path, event.segment.brief.topic)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A `Brief` is the next beat of the stream. The session asks the source for one, asks the model for a clip, and keeps a small generate-ahead buffer so you do not mint faster than the playhead.
|
|
98
|
+
|
|
99
|
+
## Core types
|
|
100
|
+
|
|
101
|
+
| Type | Role |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| **`Brief`** | Topic, talking points, citations, priority |
|
|
104
|
+
| **`Character`** | Seed still, visual prompt, voice description |
|
|
105
|
+
| **`Model`** | `generate(request) -> Segment`. `H3Max` ships first; swap the adapter to change models |
|
|
106
|
+
| **`Source`** | `next_brief(playhead) -> Brief \| None` |
|
|
107
|
+
| **`Continuity`** | Which still starts the next clip (`LastFrameContinuity` or `IdentityContinuity`) |
|
|
108
|
+
| **`Session`** | Buffer, pacing, event stream |
|
|
109
|
+
|
|
110
|
+
### Sources
|
|
111
|
+
|
|
112
|
+
- **`QueueSource`** — explicit briefs, ordered by `priority`
|
|
113
|
+
- **`FeedSource`** — RSS/Atom mapped to cited stories
|
|
114
|
+
- **`IdleSource`** — character bits that make **no news claims**
|
|
115
|
+
|
|
116
|
+
When the queue is empty, pass `idle=IdleSource(character)` so the session can keep breathing without inventing facts.
|
|
117
|
+
|
|
118
|
+
### Custom model
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from anchorcast.types import GenerateRequest, Segment
|
|
122
|
+
|
|
123
|
+
class MyModel:
|
|
124
|
+
def generate(self, request: GenerateRequest) -> Segment:
|
|
125
|
+
# write a video to request.output_path
|
|
126
|
+
return Segment(
|
|
127
|
+
path=request.output_path,
|
|
128
|
+
duration=15.0,
|
|
129
|
+
generated_in=1.0,
|
|
130
|
+
brief=request.brief,
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Custom source
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from anchorcast.types import Brief, Playhead
|
|
138
|
+
|
|
139
|
+
class WebhookSource:
|
|
140
|
+
def next_brief(self, playhead: Playhead | None = None) -> Brief | None:
|
|
141
|
+
return self._take_from_queue()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
YouTube Live, Twitch, TikTok Live, and Super Chats are a later backend. That layer should map platform events into `Brief`s and submit them to a `QueueSource`. It does not belong in this client.
|
|
145
|
+
|
|
146
|
+
## Status
|
|
147
|
+
|
|
148
|
+
- Generate-ahead `Session` with a pluggable model and source
|
|
149
|
+
- fal MiniMax H3 Max adapter (`minimax/h3-max/image-to-video`)
|
|
150
|
+
- Feed, queue, and idle sources
|
|
151
|
+
- Last-frame continuity via ffmpeg
|
|
152
|
+
|
|
153
|
+
Not yet: RTMP output, hosted APIs, or platform chat integrations.
|
|
154
|
+
|
|
155
|
+
## Example app
|
|
156
|
+
|
|
157
|
+
`examples/literary_bear` is a continuous 1926 literary-bear news stream (Shepard, not Disney). Each clip is the next fifteen seconds of one monologue, not a fresh "oh bother" cold open.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
python3.11 -m examples.literary_bear --dry-run # scripts only
|
|
161
|
+
python3.11 -m examples.literary_bear # local player at http://127.0.0.1:8765
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`--llm` or `OPENAI_API_KEY` writes spoken lines with `gpt-5.6-luna`. `--no-llm` forces the rotating templates.
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
python3.11 -m pip install -e ".[dev]"
|
|
170
|
+
python3.11 -m pytest
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT. See [LICENSE](LICENSE).
|
|
176
|
+
|
|
177
|
+
The example still under `examples/characters/literary-bear/` is an E. H. Shepard illustration from *Winnie-the-Pooh* (1926), U.S. public domain. It is not Disney, and this project is not affiliated with Disney.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Anchorcast
|
|
2
|
+
|
|
3
|
+
A Python client for **real-time media streams generated by pluggable models**.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/scrollmark/anchorcast/actions/workflows/ci.yml)
|
|
6
|
+
[](https://pypi.org/project/anchorcast/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://www.python.org/downloads/)
|
|
9
|
+
|
|
10
|
+
> Early (`0.1.0`). The session / model / source interface is the point. Expect it to move.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install anchorcast
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
From git:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install git+https://github.com/scrollmark/anchorcast.git
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For local development:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
git clone https://github.com/scrollmark/anchorcast.git
|
|
28
|
+
cd anchorcast
|
|
29
|
+
python3.11 -m pip install -e ".[dev]"
|
|
30
|
+
python3.11 -m pytest
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Requirements:** Python 3.11+, [ffmpeg](https://ffmpeg.org/) on `PATH` (for last-frame continuity), and a [fal](https://fal.ai) key if you use the H3 Max adapter (`FAL_KEY` or `FALAI_API_KEY`).
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
from anchorcast import Brief, Character, QueueSource, Session
|
|
41
|
+
from anchorcast.models import H3Max
|
|
42
|
+
|
|
43
|
+
source = QueueSource()
|
|
44
|
+
session = Session(
|
|
45
|
+
model=H3Max(),
|
|
46
|
+
character=Character(
|
|
47
|
+
name="Bear",
|
|
48
|
+
image=Path("bear.png"),
|
|
49
|
+
visual_prompt="Hand-drawn stuffed bear on cream paper.",
|
|
50
|
+
voice_prompt="warm, unhurried cadence",
|
|
51
|
+
),
|
|
52
|
+
source=source,
|
|
53
|
+
workdir=Path(".anchorcast"),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
source.submit(
|
|
57
|
+
Brief(
|
|
58
|
+
id="rates",
|
|
59
|
+
topic="mortgage rates",
|
|
60
|
+
talking_points=("Why are mortgage rates still so high?",),
|
|
61
|
+
citations=("https://example.com/rates",),
|
|
62
|
+
priority=10,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
for event in session.run(max_segments=1):
|
|
67
|
+
print(event.segment.path, event.segment.brief.topic)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A `Brief` is the next beat of the stream. The session asks the source for one, asks the model for a clip, and keeps a small generate-ahead buffer so you do not mint faster than the playhead.
|
|
71
|
+
|
|
72
|
+
## Core types
|
|
73
|
+
|
|
74
|
+
| Type | Role |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| **`Brief`** | Topic, talking points, citations, priority |
|
|
77
|
+
| **`Character`** | Seed still, visual prompt, voice description |
|
|
78
|
+
| **`Model`** | `generate(request) -> Segment`. `H3Max` ships first; swap the adapter to change models |
|
|
79
|
+
| **`Source`** | `next_brief(playhead) -> Brief \| None` |
|
|
80
|
+
| **`Continuity`** | Which still starts the next clip (`LastFrameContinuity` or `IdentityContinuity`) |
|
|
81
|
+
| **`Session`** | Buffer, pacing, event stream |
|
|
82
|
+
|
|
83
|
+
### Sources
|
|
84
|
+
|
|
85
|
+
- **`QueueSource`** — explicit briefs, ordered by `priority`
|
|
86
|
+
- **`FeedSource`** — RSS/Atom mapped to cited stories
|
|
87
|
+
- **`IdleSource`** — character bits that make **no news claims**
|
|
88
|
+
|
|
89
|
+
When the queue is empty, pass `idle=IdleSource(character)` so the session can keep breathing without inventing facts.
|
|
90
|
+
|
|
91
|
+
### Custom model
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from anchorcast.types import GenerateRequest, Segment
|
|
95
|
+
|
|
96
|
+
class MyModel:
|
|
97
|
+
def generate(self, request: GenerateRequest) -> Segment:
|
|
98
|
+
# write a video to request.output_path
|
|
99
|
+
return Segment(
|
|
100
|
+
path=request.output_path,
|
|
101
|
+
duration=15.0,
|
|
102
|
+
generated_in=1.0,
|
|
103
|
+
brief=request.brief,
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Custom source
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from anchorcast.types import Brief, Playhead
|
|
111
|
+
|
|
112
|
+
class WebhookSource:
|
|
113
|
+
def next_brief(self, playhead: Playhead | None = None) -> Brief | None:
|
|
114
|
+
return self._take_from_queue()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
YouTube Live, Twitch, TikTok Live, and Super Chats are a later backend. That layer should map platform events into `Brief`s and submit them to a `QueueSource`. It does not belong in this client.
|
|
118
|
+
|
|
119
|
+
## Status
|
|
120
|
+
|
|
121
|
+
- Generate-ahead `Session` with a pluggable model and source
|
|
122
|
+
- fal MiniMax H3 Max adapter (`minimax/h3-max/image-to-video`)
|
|
123
|
+
- Feed, queue, and idle sources
|
|
124
|
+
- Last-frame continuity via ffmpeg
|
|
125
|
+
|
|
126
|
+
Not yet: RTMP output, hosted APIs, or platform chat integrations.
|
|
127
|
+
|
|
128
|
+
## Example app
|
|
129
|
+
|
|
130
|
+
`examples/literary_bear` is a continuous 1926 literary-bear news stream (Shepard, not Disney). Each clip is the next fifteen seconds of one monologue, not a fresh "oh bother" cold open.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
python3.11 -m examples.literary_bear --dry-run # scripts only
|
|
134
|
+
python3.11 -m examples.literary_bear # local player at http://127.0.0.1:8765
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`--llm` or `OPENAI_API_KEY` writes spoken lines with `gpt-5.6-luna`. `--no-llm` forces the rotating templates.
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
python3.11 -m pip install -e ".[dev]"
|
|
143
|
+
python3.11 -m pytest
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT. See [LICENSE](LICENSE).
|
|
149
|
+
|
|
150
|
+
The example still under `examples/characters/literary-bear/` is an E. H. Shepard illustration from *Winnie-the-Pooh* (1926), U.S. public domain. It is not Disney, and this project is not affiliated with Disney.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
`0.1.x` on `main` is the only supported line.
|
|
6
|
+
|
|
7
|
+
## Reporting
|
|
8
|
+
|
|
9
|
+
Please **do not** open a public issue for a vulnerability.
|
|
10
|
+
|
|
11
|
+
Use GitHub's private reporting:
|
|
12
|
+
|
|
13
|
+
https://github.com/scrollmark/anchorcast/security/advisories/new
|
|
14
|
+
|
|
15
|
+
Include what you ran, what you expected, and a minimal reproduction if you have one.
|
|
16
|
+
|
|
17
|
+
This client talks to third-party generation APIs. Never commit API keys. `FAL_KEY` / `FALAI_API_KEY` stay in the environment.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
Binary file
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from anchorcast import Character, FeedSource, IdleSource, Session
|
|
6
|
+
from anchorcast.continuity import LastFrameContinuity
|
|
7
|
+
from anchorcast.models import H3Max
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
character = Character(
|
|
14
|
+
name="Bear",
|
|
15
|
+
image=ROOT / "examples" / "characters" / "literary-bear" / "pooh-1926.png",
|
|
16
|
+
visual_prompt=(
|
|
17
|
+
"Hand-drawn 1926 book illustration of a stuffed bear of very little brain. "
|
|
18
|
+
"Pencil and light watercolor on cream paper. No clothing."
|
|
19
|
+
),
|
|
20
|
+
voice_prompt="warm, soft, slightly posh, unhurried stuffed-bear cadence",
|
|
21
|
+
)
|
|
22
|
+
source = FeedSource("https://feeds.bbci.co.uk/news/world/rss.xml")
|
|
23
|
+
session = Session(
|
|
24
|
+
model=H3Max(),
|
|
25
|
+
character=character,
|
|
26
|
+
source=source,
|
|
27
|
+
idle=IdleSource(character),
|
|
28
|
+
workdir=ROOT / ".anchorcast",
|
|
29
|
+
continuity=LastFrameContinuity(),
|
|
30
|
+
buffer_clips=2,
|
|
31
|
+
)
|
|
32
|
+
for event in session.run(max_segments=2):
|
|
33
|
+
clip = event.segment
|
|
34
|
+
print(f"{event.index:02d} {clip.duration:.1f}s in {clip.generated_in:.1f}s {clip.brief.topic}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|