open-research-protocol 0.4.10 → 0.4.12
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.
- package/README.md +3 -0
- package/bin/orp-compute.mjs +82 -9
- package/cli/orp.py +242 -36
- package/docs/ORP_YOUTUBE_INSPECT.md +10 -1
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/scripts/orp-kernel-benchmark.py +1 -1
- package/spec/v1/youtube-source.schema.json +49 -8
package/README.md
CHANGED
|
@@ -147,7 +147,9 @@ orp pack fetch --source <git-url> --pack-id <pack-id> --install-target . --json
|
|
|
147
147
|
orp gate run --profile default --json
|
|
148
148
|
orp packet emit --profile default --json
|
|
149
149
|
orp compute decide --input orp.compute.json --json
|
|
150
|
+
orp compute decide --project-map orp.compute-map.json --point-id adult-vs-developmental-rgc-opponent --json
|
|
150
151
|
orp compute run-local --input orp.compute.json --task orp.compute.task.json --json
|
|
152
|
+
orp compute run-local --project-map orp.compute-map.json --point-id adult-vs-developmental-rgc-opponent --task orp.compute.task.json --json
|
|
151
153
|
orp report summary --json
|
|
152
154
|
```
|
|
153
155
|
|
|
@@ -157,6 +159,7 @@ These surfaces are meant to help automated systems discover ORP quickly:
|
|
|
157
159
|
- `orp home --json` returns the same landing context in machine-readable form
|
|
158
160
|
- `orp auth ...`, `orp ideas ...`, `orp world ...`, `orp checkpoint ...`, `orp runner ...`, and `orp agent ...` expose the hosted workspace surface directly through ORP
|
|
159
161
|
- `orp compute ...` exposes targeted-compute admission, local execution, and paid-approval gating through a stable ORP wrapper surface
|
|
162
|
+
- `orp compute ...` can now consume either a raw compute packet input or a repo-declared `breakthroughs` project compute map plus a compute-point id
|
|
160
163
|
- `orp youtube inspect ...` exposes public YouTube metadata plus full transcript ingestion through a stable ORP artifact shape for agent use when caption tracks are available
|
|
161
164
|
- `orp init`, `orp status`, `orp branch start`, `orp checkpoint create`, `orp backup`, `orp ready`, `orp doctor`, and `orp cleanup` expose the local-first repo governance surface directly through ORP
|
|
162
165
|
- `orp discover ...` exposes profile-based GitHub scanning as a built-in ORP ability
|
package/bin/orp-compute.mjs
CHANGED
|
@@ -4,12 +4,14 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import {
|
|
7
|
+
buildComputePointDecisionPacket,
|
|
7
8
|
buildOrpComputeGateResult,
|
|
8
9
|
buildOrpComputePacket,
|
|
9
10
|
defineComputePacket,
|
|
10
11
|
defineDecision,
|
|
11
12
|
defineImpactRead,
|
|
12
13
|
definePolicy,
|
|
14
|
+
defineProjectComputeMap,
|
|
13
15
|
defineResultBundle,
|
|
14
16
|
defineRung,
|
|
15
17
|
evaluateDispatch,
|
|
@@ -21,7 +23,9 @@ function printHelp() {
|
|
|
21
23
|
|
|
22
24
|
Usage:
|
|
23
25
|
orp compute decide --input <path> [--packet-out <path>] [--json]
|
|
26
|
+
orp compute decide --project-map <path> --point-id <id> [--rung-id <id>] [--success-bar <path>] [--packet-out <path>] [--json]
|
|
24
27
|
orp compute run-local --input <path> --task <path> [--receipt-out <path>] [--packet-out <path>] [--json]
|
|
28
|
+
orp compute run-local --project-map <path> --point-id <id> --task <path> [--rung-id <id>] [--success-bar <path>] [--receipt-out <path>] [--packet-out <path>] [--json]
|
|
25
29
|
|
|
26
30
|
Input JSON shape:
|
|
27
31
|
{
|
|
@@ -40,6 +44,22 @@ Input JSON shape:
|
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
Project-map mode:
|
|
48
|
+
{
|
|
49
|
+
"projectId": "longevity-controller",
|
|
50
|
+
"repoRoots": ["/abs/path"],
|
|
51
|
+
"rungs": [...],
|
|
52
|
+
"defaultPolicy": {...},
|
|
53
|
+
"computePoints": [...]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Project-map mode options:
|
|
57
|
+
- --project-map <path> points to a repo compute catalog
|
|
58
|
+
- --point-id <id> selects the compute point
|
|
59
|
+
- --rung-id <id> optionally overrides the point default rung
|
|
60
|
+
- --success-bar <path> optionally points to a JSON object merged into the packet success bar
|
|
61
|
+
- repo/orp context is derived from the project map unless overridden with --repo-root, --board-id, --problem-id, or --artifact-root
|
|
62
|
+
|
|
43
63
|
Task JSON shape for run-local:
|
|
44
64
|
{
|
|
45
65
|
"command": "node",
|
|
@@ -116,11 +136,71 @@ function buildContext(raw) {
|
|
|
116
136
|
};
|
|
117
137
|
}
|
|
118
138
|
|
|
139
|
+
async function loadContext(options) {
|
|
140
|
+
if (options.input && options.projectMap) {
|
|
141
|
+
throw new Error("use either --input or --project-map, not both");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (options.projectMap) {
|
|
145
|
+
if (!options.pointId) {
|
|
146
|
+
throw new Error("project-map mode requires --point-id <id>");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const projectMap = defineProjectComputeMap(await readJson(options.projectMap));
|
|
150
|
+
const successBar = options.successBar
|
|
151
|
+
? await readJson(options.successBar)
|
|
152
|
+
: undefined;
|
|
153
|
+
const template = buildComputePointDecisionPacket({
|
|
154
|
+
projectComputeMap: projectMap,
|
|
155
|
+
pointId: options.pointId,
|
|
156
|
+
rungId: options.rungId,
|
|
157
|
+
successBar,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
raw: {
|
|
162
|
+
projectMap,
|
|
163
|
+
repo: {
|
|
164
|
+
rootPath: options.repoRoot || projectMap.repoRoots[0] || process.cwd(),
|
|
165
|
+
},
|
|
166
|
+
orp: {
|
|
167
|
+
boardId: options.boardId || "targeted_compute",
|
|
168
|
+
problemId: options.problemId || template.computePoint.id,
|
|
169
|
+
artifactRoot:
|
|
170
|
+
options.artifactRoot ||
|
|
171
|
+
`orp/artifacts/compute/${template.computePoint.id}`,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
projectMap,
|
|
175
|
+
computePoint: template.computePoint,
|
|
176
|
+
decision: template.decision,
|
|
177
|
+
rung: template.rung,
|
|
178
|
+
policy: template.policy,
|
|
179
|
+
packet: template.packet,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!options.input) {
|
|
184
|
+
throw new Error("compute command requires --input <path> or --project-map <path>");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return buildContext(await readJson(options.input));
|
|
188
|
+
}
|
|
189
|
+
|
|
119
190
|
function commandLabel(subcommand, options) {
|
|
120
191
|
const parts = ["orp", "compute", subcommand];
|
|
121
192
|
if (options.input) {
|
|
122
193
|
parts.push("--input", options.input);
|
|
123
194
|
}
|
|
195
|
+
if (options.projectMap) {
|
|
196
|
+
parts.push("--project-map", options.projectMap);
|
|
197
|
+
}
|
|
198
|
+
if (options.pointId) {
|
|
199
|
+
parts.push("--point-id", options.pointId);
|
|
200
|
+
}
|
|
201
|
+
if (options.rungId) {
|
|
202
|
+
parts.push("--rung-id", options.rungId);
|
|
203
|
+
}
|
|
124
204
|
if (options.task) {
|
|
125
205
|
parts.push("--task", options.task);
|
|
126
206
|
}
|
|
@@ -148,11 +228,7 @@ function summarizeDispatch(dispatchResult) {
|
|
|
148
228
|
}
|
|
149
229
|
|
|
150
230
|
async function runDecide(options) {
|
|
151
|
-
|
|
152
|
-
throw new Error("compute decide requires --input <path>");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const context = buildContext(await readJson(options.input));
|
|
231
|
+
const context = await loadContext(options);
|
|
156
232
|
const dispatchResult = evaluateDispatch(context);
|
|
157
233
|
const gateResult = buildOrpComputeGateResult({
|
|
158
234
|
gateId: context.packet.rungId,
|
|
@@ -195,14 +271,11 @@ async function runDecide(options) {
|
|
|
195
271
|
}
|
|
196
272
|
|
|
197
273
|
async function runLocal(options) {
|
|
198
|
-
if (!options.input) {
|
|
199
|
-
throw new Error("compute run-local requires --input <path>");
|
|
200
|
-
}
|
|
201
274
|
if (!options.task) {
|
|
202
275
|
throw new Error("compute run-local requires --task <path>");
|
|
203
276
|
}
|
|
204
277
|
|
|
205
|
-
const context =
|
|
278
|
+
const context = await loadContext(options);
|
|
206
279
|
const task = await readJson(options.task);
|
|
207
280
|
const dispatchResult = evaluateDispatch(context);
|
|
208
281
|
|
package/cli/orp.py
CHANGED
|
@@ -115,6 +115,10 @@ DEFAULT_DISCOVER_SCAN_ROOT = "orp/discovery/github"
|
|
|
115
115
|
DEFAULT_HOSTED_BASE_URL = "https://orp.earth"
|
|
116
116
|
KERNEL_SCHEMA_VERSION = "1.0.0"
|
|
117
117
|
YOUTUBE_SOURCE_SCHEMA_VERSION = "1.0.0"
|
|
118
|
+
YOUTUBE_ANDROID_CLIENT_VERSION = "20.10.38"
|
|
119
|
+
YOUTUBE_ANDROID_USER_AGENT = (
|
|
120
|
+
f"com.google.android.youtube/{YOUTUBE_ANDROID_CLIENT_VERSION} (Linux; U; Android 14)"
|
|
121
|
+
)
|
|
118
122
|
|
|
119
123
|
|
|
120
124
|
class HostedApiError(RuntimeError):
|
|
@@ -362,6 +366,35 @@ def _http_get_json(url: str, *, headers: dict[str, str] | None = None, timeout_s
|
|
|
362
366
|
raise RuntimeError(f"Response from {url} was not a JSON object.")
|
|
363
367
|
|
|
364
368
|
|
|
369
|
+
def _http_post_json(
|
|
370
|
+
url: str,
|
|
371
|
+
payload: dict[str, Any],
|
|
372
|
+
*,
|
|
373
|
+
headers: dict[str, str] | None = None,
|
|
374
|
+
timeout_sec: int = 20,
|
|
375
|
+
) -> dict[str, Any]:
|
|
376
|
+
body = json.dumps(payload).encode("utf-8")
|
|
377
|
+
merged_headers = {"Content-Type": "application/json"}
|
|
378
|
+
if headers:
|
|
379
|
+
merged_headers.update(headers)
|
|
380
|
+
request = urlrequest.Request(url, data=body, headers=merged_headers, method="POST")
|
|
381
|
+
try:
|
|
382
|
+
with urlrequest.urlopen(request, timeout=timeout_sec) as response:
|
|
383
|
+
text = response.read().decode("utf-8", errors="replace")
|
|
384
|
+
except urlerror.HTTPError as exc:
|
|
385
|
+
body_text = exc.read().decode("utf-8", errors="replace").strip()
|
|
386
|
+
raise RuntimeError(f"HTTP {exc.code} while fetching {url}: {body_text or exc.reason}") from exc
|
|
387
|
+
except urlerror.URLError as exc:
|
|
388
|
+
raise RuntimeError(f"Could not reach {url}: {exc.reason}") from exc
|
|
389
|
+
try:
|
|
390
|
+
parsed = json.loads(text)
|
|
391
|
+
except Exception as exc:
|
|
392
|
+
raise RuntimeError(f"Response from {url} was not valid JSON.") from exc
|
|
393
|
+
if isinstance(parsed, dict):
|
|
394
|
+
return parsed
|
|
395
|
+
raise RuntimeError(f"Response from {url} was not a JSON object.")
|
|
396
|
+
|
|
397
|
+
|
|
365
398
|
def _youtube_request_headers() -> dict[str, str]:
|
|
366
399
|
return {
|
|
367
400
|
"User-Agent": (
|
|
@@ -372,6 +405,13 @@ def _youtube_request_headers() -> dict[str, str]:
|
|
|
372
405
|
}
|
|
373
406
|
|
|
374
407
|
|
|
408
|
+
def _youtube_android_request_headers() -> dict[str, str]:
|
|
409
|
+
return {
|
|
410
|
+
"User-Agent": YOUTUBE_ANDROID_USER_AGENT,
|
|
411
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
|
|
375
415
|
def _youtube_source_schema_path() -> Path:
|
|
376
416
|
return Path(__file__).resolve().parent.parent / "spec" / "v1" / "youtube-source.schema.json"
|
|
377
417
|
|
|
@@ -459,21 +499,52 @@ def _youtube_track_label(track: dict[str, Any]) -> str:
|
|
|
459
499
|
return str(track.get("languageCode", "")).strip()
|
|
460
500
|
|
|
461
501
|
|
|
462
|
-
def
|
|
463
|
-
|
|
464
|
-
|
|
502
|
+
def _youtube_track_source(track: dict[str, Any]) -> str:
|
|
503
|
+
return str(track.get("_orp_source", "") or "unknown").strip()
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _youtube_track_inventory(tracks: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
507
|
+
inventory: list[dict[str, Any]] = []
|
|
508
|
+
seen: set[tuple[str, str, str, str]] = set()
|
|
509
|
+
for track in tracks:
|
|
510
|
+
if not isinstance(track, dict):
|
|
511
|
+
continue
|
|
512
|
+
language_code = str(track.get("languageCode", "")).strip()
|
|
513
|
+
label = _youtube_track_label(track)
|
|
514
|
+
kind = "auto" if str(track.get("kind", "")).strip().lower() == "asr" else "manual"
|
|
515
|
+
source = _youtube_track_source(track)
|
|
516
|
+
key = (language_code, label, kind, source)
|
|
517
|
+
if key in seen:
|
|
518
|
+
continue
|
|
519
|
+
seen.add(key)
|
|
520
|
+
inventory.append(
|
|
521
|
+
{
|
|
522
|
+
"language_code": language_code,
|
|
523
|
+
"name": label,
|
|
524
|
+
"kind": kind,
|
|
525
|
+
"source": source,
|
|
526
|
+
}
|
|
527
|
+
)
|
|
528
|
+
return inventory
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _youtube_caption_track_sort_key(track: dict[str, Any], preferred_lang: str = "") -> tuple[int, int]:
|
|
465
532
|
preferred = str(preferred_lang or "").strip().lower()
|
|
533
|
+
code = str(track.get("languageCode", "")).strip().lower()
|
|
534
|
+
kind = str(track.get("kind", "")).strip().lower()
|
|
535
|
+
auto = 1 if kind == "asr" else 0
|
|
536
|
+
source = _youtube_track_source(track)
|
|
537
|
+
source_bias = 15 if source == "android_player" else 0
|
|
538
|
+
exact = 1 if preferred and code == preferred else 0
|
|
539
|
+
prefix = 1 if preferred and code.startswith(preferred + "-") else 0
|
|
540
|
+
english = 1 if code.startswith("en") else 0
|
|
541
|
+
return (exact * 100 + prefix * 80 + english * 20 + source_bias - auto * 5, -auto)
|
|
466
542
|
|
|
467
|
-
def score(track: dict[str, Any]) -> tuple[int, int]:
|
|
468
|
-
code = str(track.get("languageCode", "")).strip().lower()
|
|
469
|
-
kind = str(track.get("kind", "")).strip().lower()
|
|
470
|
-
auto = 1 if kind == "asr" else 0
|
|
471
|
-
exact = 1 if preferred and code == preferred else 0
|
|
472
|
-
prefix = 1 if preferred and code.startswith(preferred + "-") else 0
|
|
473
|
-
english = 1 if code.startswith("en") else 0
|
|
474
|
-
return (exact * 100 + prefix * 80 + english * 20 - auto * 5, -auto)
|
|
475
543
|
|
|
476
|
-
|
|
544
|
+
def _pick_youtube_caption_track(tracks: list[dict[str, Any]], preferred_lang: str = "") -> dict[str, Any] | None:
|
|
545
|
+
if not tracks:
|
|
546
|
+
return None
|
|
547
|
+
ranked = sorted(tracks, key=lambda track: _youtube_caption_track_sort_key(track, preferred_lang), reverse=True)
|
|
477
548
|
return ranked[0] if ranked else None
|
|
478
549
|
|
|
479
550
|
|
|
@@ -544,6 +615,19 @@ def _parse_youtube_transcript_xml(text: str) -> tuple[str, list[dict[str, Any]]]
|
|
|
544
615
|
"text": body,
|
|
545
616
|
}
|
|
546
617
|
)
|
|
618
|
+
if not segments:
|
|
619
|
+
for node in root.findall(".//p"):
|
|
620
|
+
body = html.unescape("".join(node.itertext() or []))
|
|
621
|
+
body = re.sub(r"\s+", " ", body).strip()
|
|
622
|
+
if not body:
|
|
623
|
+
continue
|
|
624
|
+
segments.append(
|
|
625
|
+
{
|
|
626
|
+
"start_ms": int(node.attrib.get("t", "0") or "0"),
|
|
627
|
+
"duration_ms": int(node.attrib.get("d", "0") or "0"),
|
|
628
|
+
"text": body,
|
|
629
|
+
}
|
|
630
|
+
)
|
|
547
631
|
transcript_text = "\n".join(str(row["text"]) for row in segments)
|
|
548
632
|
return transcript_text, segments
|
|
549
633
|
|
|
@@ -577,6 +661,8 @@ def _youtube_fetch_watch_state(video_id: str) -> dict[str, Any]:
|
|
|
577
661
|
.get("playerCaptionsTracklistRenderer", {})
|
|
578
662
|
.get("captionTracks", [])
|
|
579
663
|
)
|
|
664
|
+
tracks = captions if isinstance(captions, list) else []
|
|
665
|
+
normalized_tracks = [{**row, "_orp_source": "watch_page"} for row in tracks if isinstance(row, dict)]
|
|
580
666
|
return {
|
|
581
667
|
"player_response": player_response,
|
|
582
668
|
"video_details": player_response.get("videoDetails", {}) if isinstance(player_response.get("videoDetails"), dict) else {},
|
|
@@ -590,29 +676,110 @@ def _youtube_fetch_watch_state(video_id: str) -> dict[str, Any]:
|
|
|
590
676
|
if isinstance(player_response.get("playabilityStatus"), dict)
|
|
591
677
|
else {}
|
|
592
678
|
),
|
|
593
|
-
"caption_tracks":
|
|
679
|
+
"caption_tracks": normalized_tracks,
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _youtube_fetch_android_player_state(video_id: str) -> dict[str, Any]:
|
|
684
|
+
payload = _http_post_json(
|
|
685
|
+
"https://www.youtube.com/youtubei/v1/player?prettyPrint=false",
|
|
686
|
+
{
|
|
687
|
+
"context": {
|
|
688
|
+
"client": {
|
|
689
|
+
"clientName": "ANDROID",
|
|
690
|
+
"clientVersion": YOUTUBE_ANDROID_CLIENT_VERSION,
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
"videoId": video_id,
|
|
694
|
+
},
|
|
695
|
+
headers=_youtube_android_request_headers(),
|
|
696
|
+
timeout_sec=25,
|
|
697
|
+
)
|
|
698
|
+
captions = (
|
|
699
|
+
payload.get("captions", {})
|
|
700
|
+
.get("playerCaptionsTracklistRenderer", {})
|
|
701
|
+
.get("captionTracks", [])
|
|
702
|
+
)
|
|
703
|
+
tracks = captions if isinstance(captions, list) else []
|
|
704
|
+
normalized_tracks = [{**row, "_orp_source": "android_player"} for row in tracks if isinstance(row, dict)]
|
|
705
|
+
return {
|
|
706
|
+
"player_response": payload,
|
|
707
|
+
"video_details": payload.get("videoDetails", {}) if isinstance(payload.get("videoDetails"), dict) else {},
|
|
708
|
+
"microformat": {},
|
|
709
|
+
"playability_status": payload.get("playabilityStatus", {}) if isinstance(payload.get("playabilityStatus"), dict) else {},
|
|
710
|
+
"caption_tracks": normalized_tracks,
|
|
594
711
|
}
|
|
595
712
|
|
|
596
713
|
|
|
714
|
+
def _youtube_ranked_caption_tracks(
|
|
715
|
+
watch_tracks: list[dict[str, Any]],
|
|
716
|
+
android_tracks: list[dict[str, Any]],
|
|
717
|
+
preferred_lang: str = "",
|
|
718
|
+
) -> list[dict[str, Any]]:
|
|
719
|
+
ranked = sorted(
|
|
720
|
+
[track for track in android_tracks if isinstance(track, dict)]
|
|
721
|
+
+ [track for track in watch_tracks if isinstance(track, dict)],
|
|
722
|
+
key=lambda track: _youtube_caption_track_sort_key(track, preferred_lang),
|
|
723
|
+
reverse=True,
|
|
724
|
+
)
|
|
725
|
+
unique: list[dict[str, Any]] = []
|
|
726
|
+
seen: set[tuple[str, str, str, str]] = set()
|
|
727
|
+
for track in ranked:
|
|
728
|
+
key = (
|
|
729
|
+
str(track.get("languageCode", "")).strip(),
|
|
730
|
+
_youtube_track_label(track),
|
|
731
|
+
str(track.get("kind", "")).strip().lower(),
|
|
732
|
+
_youtube_track_source(track),
|
|
733
|
+
)
|
|
734
|
+
if key in seen:
|
|
735
|
+
continue
|
|
736
|
+
seen.add(key)
|
|
737
|
+
unique.append(track)
|
|
738
|
+
return unique
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _youtube_parse_transcript_response(text: str) -> tuple[str, list[dict[str, Any]], str]:
|
|
742
|
+
stripped = str(text or "").lstrip()
|
|
743
|
+
if not stripped:
|
|
744
|
+
return ("", [], "empty")
|
|
745
|
+
if stripped.startswith("{"):
|
|
746
|
+
try:
|
|
747
|
+
payload = json.loads(text)
|
|
748
|
+
except Exception:
|
|
749
|
+
payload = None
|
|
750
|
+
if isinstance(payload, dict):
|
|
751
|
+
transcript_text, segments = _parse_youtube_transcript_json3(payload)
|
|
752
|
+
if transcript_text:
|
|
753
|
+
return (transcript_text, segments, "json3")
|
|
754
|
+
transcript_text, segments = _parse_youtube_transcript_xml(text)
|
|
755
|
+
if transcript_text:
|
|
756
|
+
return (transcript_text, segments, "xml")
|
|
757
|
+
return ("", [], "unparsed")
|
|
758
|
+
|
|
759
|
+
|
|
597
760
|
def _youtube_fetch_transcript_from_track(track: dict[str, Any]) -> tuple[str, list[dict[str, Any]], str]:
|
|
598
761
|
base_url = str(track.get("baseUrl", "")).strip()
|
|
599
762
|
if not base_url:
|
|
600
763
|
return ("", [], "missing_track_url")
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
764
|
+
source = _youtube_track_source(track) or "unknown"
|
|
765
|
+
candidate_urls = [
|
|
766
|
+
("base", base_url),
|
|
767
|
+
("json3", _youtube_add_query_param(base_url, "fmt", "json3")),
|
|
768
|
+
("srv3", _youtube_add_query_param(base_url, "fmt", "srv3")),
|
|
769
|
+
]
|
|
770
|
+
seen_urls: set[str] = set()
|
|
771
|
+
for mode, candidate_url in candidate_urls:
|
|
772
|
+
if candidate_url in seen_urls:
|
|
773
|
+
continue
|
|
774
|
+
seen_urls.add(candidate_url)
|
|
775
|
+
try:
|
|
776
|
+
response_text = _http_get_text(candidate_url, headers=_youtube_request_headers(), timeout_sec=25)
|
|
777
|
+
except Exception:
|
|
778
|
+
continue
|
|
779
|
+
transcript_text, segments, parsed_mode = _youtube_parse_transcript_response(response_text)
|
|
612
780
|
if transcript_text:
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
pass
|
|
781
|
+
final_mode = parsed_mode if mode == "base" else f"{mode}_{parsed_mode}"
|
|
782
|
+
return transcript_text, segments, f"{source}_{final_mode}"
|
|
616
783
|
return ("", [], "unavailable")
|
|
617
784
|
|
|
618
785
|
|
|
@@ -647,28 +814,61 @@ def _youtube_inspect_payload(raw_url: str, preferred_lang: str = "") -> dict[str
|
|
|
647
814
|
watch_state = _youtube_fetch_watch_state(video_id)
|
|
648
815
|
except Exception as exc:
|
|
649
816
|
warnings.append(str(exc))
|
|
817
|
+
android_state: dict[str, Any] = {}
|
|
818
|
+
try:
|
|
819
|
+
android_state = _youtube_fetch_android_player_state(video_id)
|
|
820
|
+
except Exception as exc:
|
|
821
|
+
warnings.append(str(exc))
|
|
650
822
|
|
|
651
|
-
|
|
823
|
+
watch_video_details = watch_state.get("video_details", {}) if isinstance(watch_state.get("video_details"), dict) else {}
|
|
824
|
+
android_video_details = (
|
|
825
|
+
android_state.get("video_details", {}) if isinstance(android_state.get("video_details"), dict) else {}
|
|
826
|
+
)
|
|
827
|
+
video_details = watch_video_details or android_video_details
|
|
652
828
|
microformat = watch_state.get("microformat", {}) if isinstance(watch_state.get("microformat"), dict) else {}
|
|
653
829
|
playability = watch_state.get("playability_status", {}) if isinstance(watch_state.get("playability_status"), dict) else {}
|
|
654
|
-
|
|
655
|
-
|
|
830
|
+
if not playability:
|
|
831
|
+
playability = android_state.get("playability_status", {}) if isinstance(android_state.get("playability_status"), dict) else {}
|
|
832
|
+
watch_tracks = [row for row in watch_state.get("caption_tracks", []) if isinstance(row, dict)]
|
|
833
|
+
android_tracks = [row for row in android_state.get("caption_tracks", []) if isinstance(row, dict)]
|
|
834
|
+
tracks = _youtube_ranked_caption_tracks(watch_tracks, android_tracks, preferred_lang)
|
|
835
|
+
available_tracks = _youtube_track_inventory(tracks)
|
|
656
836
|
transcript_text = ""
|
|
657
837
|
transcript_segments: list[dict[str, Any]] = []
|
|
658
838
|
transcript_fetch_mode = "none"
|
|
659
839
|
transcript_available = False
|
|
660
840
|
transcript_language = ""
|
|
661
841
|
transcript_track_name = ""
|
|
842
|
+
transcript_track_source = ""
|
|
662
843
|
transcript_kind = "none"
|
|
844
|
+
transcript_sources_tried: list[str] = []
|
|
845
|
+
chosen_track: dict[str, Any] | None = None
|
|
846
|
+
for candidate in tracks:
|
|
847
|
+
transcript_sources_tried.append(
|
|
848
|
+
":".join(
|
|
849
|
+
part
|
|
850
|
+
for part in [
|
|
851
|
+
_youtube_track_source(candidate),
|
|
852
|
+
str(candidate.get("languageCode", "")).strip(),
|
|
853
|
+
_youtube_track_label(candidate),
|
|
854
|
+
]
|
|
855
|
+
if part
|
|
856
|
+
)
|
|
857
|
+
)
|
|
858
|
+
transcript_text, transcript_segments, transcript_fetch_mode = _youtube_fetch_transcript_from_track(candidate)
|
|
859
|
+
if transcript_text.strip():
|
|
860
|
+
transcript_available = True
|
|
861
|
+
chosen_track = candidate
|
|
862
|
+
break
|
|
663
863
|
if chosen_track is not None:
|
|
664
|
-
transcript_text, transcript_segments, transcript_fetch_mode = _youtube_fetch_transcript_from_track(chosen_track)
|
|
665
|
-
transcript_available = bool(transcript_text.strip())
|
|
666
864
|
transcript_language = str(chosen_track.get("languageCode", "")).strip()
|
|
667
865
|
transcript_track_name = _youtube_track_label(chosen_track)
|
|
866
|
+
transcript_track_source = _youtube_track_source(chosen_track)
|
|
668
867
|
transcript_kind = "auto" if str(chosen_track.get("kind", "")).strip().lower() == "asr" else "manual"
|
|
868
|
+
if tracks:
|
|
669
869
|
if not transcript_available:
|
|
670
870
|
warnings.append("A caption track was found, but transcript text could not be fetched.")
|
|
671
|
-
elif watch_state:
|
|
871
|
+
elif watch_state or android_state:
|
|
672
872
|
warnings.append("No caption tracks were available for this video.")
|
|
673
873
|
|
|
674
874
|
title = str(video_details.get("title") or oembed.get("title") or "").strip()
|
|
@@ -698,13 +898,17 @@ def _youtube_inspect_payload(raw_url: str, preferred_lang: str = "") -> dict[str
|
|
|
698
898
|
"duration_seconds": duration_seconds or None,
|
|
699
899
|
"published_at": published_at,
|
|
700
900
|
"playability_status": str(playability.get("status", "")).strip(),
|
|
901
|
+
"transcript_track_count": len(available_tracks),
|
|
902
|
+
"available_transcript_tracks": available_tracks,
|
|
701
903
|
"transcript_available": transcript_available,
|
|
702
904
|
"transcript_language": transcript_language,
|
|
703
905
|
"transcript_track_name": transcript_track_name,
|
|
906
|
+
"transcript_track_source": transcript_track_source,
|
|
704
907
|
"transcript_kind": transcript_kind,
|
|
705
908
|
"transcript_fetch_mode": transcript_fetch_mode,
|
|
706
909
|
"transcript_text": transcript_text,
|
|
707
910
|
"transcript_segments": transcript_segments,
|
|
911
|
+
"transcript_sources_tried": transcript_sources_tried,
|
|
708
912
|
"warnings": _unique_strings(warnings),
|
|
709
913
|
}
|
|
710
914
|
payload["text_bundle"] = _youtube_text_bundle(payload)
|
|
@@ -754,8 +958,10 @@ def cmd_youtube_inspect(args: argparse.Namespace) -> int:
|
|
|
754
958
|
("video.title", str(payload.get("title", "")).strip()),
|
|
755
959
|
("video.author", str(payload.get("author_name", "")).strip()),
|
|
756
960
|
("video.duration_seconds", payload.get("duration_seconds") or ""),
|
|
961
|
+
("transcript.track_count", payload.get("transcript_track_count") or 0),
|
|
757
962
|
("transcript.available", str(bool(payload.get("transcript_available", False))).lower()),
|
|
758
963
|
("transcript.language", str(payload.get("transcript_language", "")).strip()),
|
|
964
|
+
("transcript.track_source", str(payload.get("transcript_track_source", "")).strip()),
|
|
759
965
|
("transcript.kind", str(payload.get("transcript_kind", "")).strip()),
|
|
760
966
|
("saved", str(bool(out_path is not None)).lower()),
|
|
761
967
|
("path", _path_for_state(out_path, repo_root) if out_path is not None else ""),
|
|
@@ -5774,7 +5980,7 @@ def _about_payload() -> dict[str, Any]:
|
|
|
5774
5980
|
"Default CLI output is human-readable; listed commands with json_output=true also support --json.",
|
|
5775
5981
|
"Reasoning-kernel artifacts shape promotable repository truth for tasks, decisions, hypotheses, experiments, checkpoints, policies, and results.",
|
|
5776
5982
|
"Kernel evolution in ORP should stay explicit: observe real usage, propose changes, and migrate artifacts through versioned CLI surfaces rather than silent agent mutation.",
|
|
5777
|
-
"YouTube inspection is a built-in ORP ability exposed through `orp youtube inspect`, returning public metadata
|
|
5983
|
+
"YouTube inspection is a built-in ORP ability exposed through `orp youtube inspect`, returning public metadata plus full transcript text and segments whenever public caption tracks are available.",
|
|
5778
5984
|
"Discovery profiles in ORP are portable search-intent files managed directly by ORP.",
|
|
5779
5985
|
"Collaboration is a built-in ORP ability exposed through `orp collaborate ...`.",
|
|
5780
5986
|
"Project/session linking is a built-in ORP ability exposed through `orp link ...` and stored machine-locally under `.git/orp/link/`.",
|
|
@@ -5885,7 +6091,7 @@ def _home_payload(repo_root: Path, config_arg: str) -> dict[str, Any]:
|
|
|
5885
6091
|
"command": "orp whoami --json",
|
|
5886
6092
|
},
|
|
5887
6093
|
{
|
|
5888
|
-
"label": "Inspect a YouTube video and public transcript
|
|
6094
|
+
"label": "Inspect a YouTube video and ingest full public transcript context",
|
|
5889
6095
|
"command": "orp youtube inspect https://www.youtube.com/watch?v=<video_id> --json",
|
|
5890
6096
|
},
|
|
5891
6097
|
{
|
|
@@ -12715,7 +12921,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
12715
12921
|
|
|
12716
12922
|
s_youtube_inspect = youtube_sub.add_parser(
|
|
12717
12923
|
"inspect",
|
|
12718
|
-
help="Inspect a YouTube video and fetch public metadata plus transcript text when
|
|
12924
|
+
help="Inspect a YouTube video and fetch public metadata plus full transcript text and segments when caption tracks are available",
|
|
12719
12925
|
)
|
|
12720
12926
|
s_youtube_inspect.add_argument("url", help="YouTube watch/share URL or 11-character video id")
|
|
12721
12927
|
s_youtube_inspect.add_argument(
|
|
@@ -6,7 +6,7 @@ YouTube videos.
|
|
|
6
6
|
It gives agents and users a stable way to turn a YouTube link into:
|
|
7
7
|
|
|
8
8
|
- normalized video metadata,
|
|
9
|
-
- public
|
|
9
|
+
- full public transcript text and segment timing when caption tracks are available,
|
|
10
10
|
- segment-level timing rows,
|
|
11
11
|
- and one agent-friendly `text_bundle` field that can be handed directly into
|
|
12
12
|
summarization, extraction, comparison, or kernel-shaped artifact creation.
|
|
@@ -61,13 +61,17 @@ The command returns:
|
|
|
61
61
|
- `published_at`
|
|
62
62
|
- `playability_status`
|
|
63
63
|
- transcript fields:
|
|
64
|
+
- `transcript_track_count`
|
|
65
|
+
- `available_transcript_tracks`
|
|
64
66
|
- `transcript_available`
|
|
65
67
|
- `transcript_language`
|
|
66
68
|
- `transcript_track_name`
|
|
69
|
+
- `transcript_track_source`
|
|
67
70
|
- `transcript_kind`
|
|
68
71
|
- `transcript_fetch_mode`
|
|
69
72
|
- `transcript_text`
|
|
70
73
|
- `transcript_segments`
|
|
74
|
+
- `transcript_sources_tried`
|
|
71
75
|
- agent-ready bundle:
|
|
72
76
|
- `text_bundle`
|
|
73
77
|
- capture notes:
|
|
@@ -89,6 +93,11 @@ discipline while staying outside the evidence boundary by default.
|
|
|
89
93
|
`orp youtube inspect` returns public source context. It does **not** make the
|
|
90
94
|
result canonical evidence by itself.
|
|
91
95
|
|
|
96
|
+
When public caption tracks exist, ORP now attempts full transcript ingestion
|
|
97
|
+
across multiple retrieval strategies and records which track/source succeeded.
|
|
98
|
+
If a video has no accessible caption tracks, ORP reports that honestly instead
|
|
99
|
+
of silently fabricating a transcript.
|
|
100
|
+
|
|
92
101
|
If a video matters for repo truth, the agent should still:
|
|
93
102
|
|
|
94
103
|
1. inspect the video,
|
package/llms.txt
CHANGED
|
@@ -13,7 +13,7 @@ ORP (Open Research Protocol) is a docs-first, local-first, agent-friendly protoc
|
|
|
13
13
|
## Fast Machine Discovery
|
|
14
14
|
|
|
15
15
|
- Run `orp about --json` for machine-readable tool metadata, artifact paths, schemas, supported commands, and bundled packs.
|
|
16
|
-
- Run `orp youtube inspect <youtube-url> --json` to normalize a public YouTube video into ORP's source artifact shape, including transcript text when public
|
|
16
|
+
- Run `orp youtube inspect <youtube-url> --json` to normalize a public YouTube video into ORP's source artifact shape, including full transcript text and timing segments when public caption tracks are available.
|
|
17
17
|
- Run `orp erdos sync --json` for machine-readable Erdos catalog sync results.
|
|
18
18
|
- Run `orp pack list --json` for machine-readable bundled pack inventory.
|
|
19
19
|
- Core runtime commands also support `--json`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-research-protocol",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"description": "ORP CLI (Open Research Protocol): agent-friendly research workflows, runtime, reports, and pack tooling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"node": ">=18"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"breakthroughs": "^0.1.
|
|
39
|
+
"breakthroughs": "^0.1.1"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"postinstall": "node scripts/npm-postinstall-check.js",
|
|
@@ -20,13 +20,17 @@
|
|
|
20
20
|
"duration_seconds",
|
|
21
21
|
"published_at",
|
|
22
22
|
"playability_status",
|
|
23
|
+
"transcript_track_count",
|
|
24
|
+
"available_transcript_tracks",
|
|
23
25
|
"transcript_available",
|
|
24
26
|
"transcript_language",
|
|
25
27
|
"transcript_track_name",
|
|
28
|
+
"transcript_track_source",
|
|
26
29
|
"transcript_kind",
|
|
27
30
|
"transcript_fetch_mode",
|
|
28
31
|
"transcript_text",
|
|
29
32
|
"transcript_segments",
|
|
33
|
+
"transcript_sources_tried",
|
|
30
34
|
"warnings",
|
|
31
35
|
"text_bundle"
|
|
32
36
|
],
|
|
@@ -83,6 +87,41 @@
|
|
|
83
87
|
"playability_status": {
|
|
84
88
|
"type": "string"
|
|
85
89
|
},
|
|
90
|
+
"transcript_track_count": {
|
|
91
|
+
"type": "integer",
|
|
92
|
+
"minimum": 0
|
|
93
|
+
},
|
|
94
|
+
"available_transcript_tracks": {
|
|
95
|
+
"type": "array",
|
|
96
|
+
"items": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"additionalProperties": false,
|
|
99
|
+
"required": [
|
|
100
|
+
"language_code",
|
|
101
|
+
"name",
|
|
102
|
+
"kind",
|
|
103
|
+
"source"
|
|
104
|
+
],
|
|
105
|
+
"properties": {
|
|
106
|
+
"language_code": {
|
|
107
|
+
"type": "string"
|
|
108
|
+
},
|
|
109
|
+
"name": {
|
|
110
|
+
"type": "string"
|
|
111
|
+
},
|
|
112
|
+
"kind": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"enum": [
|
|
115
|
+
"manual",
|
|
116
|
+
"auto"
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"source": {
|
|
120
|
+
"type": "string"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
86
125
|
"transcript_available": {
|
|
87
126
|
"type": "boolean"
|
|
88
127
|
},
|
|
@@ -92,6 +131,9 @@
|
|
|
92
131
|
"transcript_track_name": {
|
|
93
132
|
"type": "string"
|
|
94
133
|
},
|
|
134
|
+
"transcript_track_source": {
|
|
135
|
+
"type": "string"
|
|
136
|
+
},
|
|
95
137
|
"transcript_kind": {
|
|
96
138
|
"type": "string",
|
|
97
139
|
"enum": [
|
|
@@ -101,14 +143,7 @@
|
|
|
101
143
|
]
|
|
102
144
|
},
|
|
103
145
|
"transcript_fetch_mode": {
|
|
104
|
-
"type": "string"
|
|
105
|
-
"enum": [
|
|
106
|
-
"json3",
|
|
107
|
-
"xml",
|
|
108
|
-
"unavailable",
|
|
109
|
-
"none",
|
|
110
|
-
"missing_track_url"
|
|
111
|
-
]
|
|
146
|
+
"type": "string"
|
|
112
147
|
},
|
|
113
148
|
"transcript_text": {
|
|
114
149
|
"type": "string"
|
|
@@ -138,6 +173,12 @@
|
|
|
138
173
|
}
|
|
139
174
|
}
|
|
140
175
|
},
|
|
176
|
+
"transcript_sources_tried": {
|
|
177
|
+
"type": "array",
|
|
178
|
+
"items": {
|
|
179
|
+
"type": "string"
|
|
180
|
+
}
|
|
181
|
+
},
|
|
141
182
|
"warnings": {
|
|
142
183
|
"type": "array",
|
|
143
184
|
"items": {
|