Skip to content

Commit 988dea3

Browse files
Add script to quickly inject a transcript (#171)
# Summary Simple script to just inject some transcript from a json. Util to help with testing without live transcription. ## Testing - [X] I have tested these changes locally - [X] I have run all relevant automated tests - [X] I have verified this does not break existing workflows ## How I Tested Used successfully locally Signed-off-by: James Spadafora <spadjv@gmail.com> Co-authored-by: Cameron Target <127965975+camerontarget14@users.noreply.github.com>
1 parent 405d062 commit 988dea3

3 files changed

Lines changed: 231 additions & 1 deletion

File tree

backend/makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,10 @@ format-python: venv-lint
5656
.venv-lint/bin/isort src/ tests/
5757

5858
seed-mock-db:
59-
$(DOCKER_COMPOSE) -f docker-compose.yml -f docker-compose.local.yml run --rm api python -m dna.prodtrack_providers.mock_data.seed_db --project-id 124 --url https://aswf.shotgrid.autodesk.com --script-name DNA_local_testing --api-key '$(SHOTGRID_API_KEY)'
59+
$(DOCKER_COMPOSE) -f docker-compose.yml -f docker-compose.local.yml run --rm api python -m dna.prodtrack_providers.mock_data.seed_db --project-id 124 --url https://aswf.shotgrid.autodesk.com --script-name DNA_local_testing --api-key '$(SHOTGRID_API_KEY)'
60+
61+
inject-transcript:
62+
$(DOCKER_COMPOSE) -f docker-compose.yml -f docker-compose.local.yml run --rm \
63+
-v "$(abspath $(FILE)):/tmp/inject_input.json:ro" \
64+
-v "$(abspath ../scripts/inject_transcript.py):/tmp/inject_transcript.py:ro" \
65+
api python /tmp/inject_transcript.py /tmp/inject_input.json $(if $(DRYRUN),--dry-run,)

scripts/inject_transcript.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#!/usr/bin/env python3
2+
"""Inject transcript segments directly into DNA storage for one or more playlist versions.
3+
4+
Reads a JSON file structured as:
5+
6+
{
7+
"<playlist_id>": {
8+
"<version_id>": {
9+
"segments": [
10+
{
11+
"segment_id": "unique-id",
12+
"text": "Hello world.",
13+
"speaker": "Alice",
14+
"absolute_start_time": "2026-06-22T10:00:00.000Z",
15+
"absolute_end_time": "2026-06-22T10:00:02.000Z",
16+
"language": "en",
17+
"start_time": 0.0,
18+
"end_time": 2.0
19+
}
20+
]
21+
}
22+
}
23+
}
24+
25+
Required segment fields: segment_id, text, absolute_start_time, absolute_end_time.
26+
Optional: speaker, language, start_time, end_time, completed, vexa_updated_at.
27+
28+
The script must be run from within the backend environment (e.g. inside the API container
29+
or with the backend virtualenv active) so that the dna package is importable.
30+
31+
Examples:
32+
# Inside the running api container:
33+
docker exec -it dna-api python /scripts/inject_transcript.py /scripts/inject_transcript_sample.json
34+
35+
# Via docker-compose run:
36+
docker-compose run --rm api python /app/scripts/inject_transcript.py /app/scripts/inject_transcript_sample.json
37+
38+
# With a local venv:
39+
MONGODB_URL=mongodb://localhost:27017 python scripts/inject_transcript.py scripts/inject_transcript_sample.json
40+
41+
Environment:
42+
MONGODB_URL MongoDB connection string (default: mongodb://localhost:27017)
43+
"""
44+
45+
from __future__ import annotations
46+
47+
import argparse
48+
import asyncio
49+
import json
50+
import os
51+
import sys
52+
from pathlib import Path
53+
from typing import Any
54+
55+
# Ensure the backend src directory is on the path when running outside of the
56+
# installed package (e.g. directly from the repo root).
57+
_REPO_ROOT = Path(__file__).resolve().parent.parent
58+
_BACKEND_SRC = _REPO_ROOT / "backend" / "src"
59+
if _BACKEND_SRC.is_dir() and str(_BACKEND_SRC) not in sys.path:
60+
sys.path.insert(0, str(_BACKEND_SRC))
61+
62+
from dna.models.stored_segment import StoredSegmentCreate
63+
from dna.storage_providers.storage_provider_base import get_storage_provider
64+
65+
66+
def _validate_segment(seg: Any, idx: int) -> None:
67+
required = ("segment_id", "text", "absolute_start_time", "absolute_end_time")
68+
if not isinstance(seg, dict):
69+
raise SystemExit(f"Segment at index {idx} is not a JSON object: {seg!r}")
70+
for field in required:
71+
if not seg.get(field):
72+
raise SystemExit(
73+
f"Segment at index {idx} is missing required field '{field}'"
74+
)
75+
76+
77+
async def _inject(payload: dict, dry_run: bool) -> None:
78+
storage = get_storage_provider()
79+
80+
total_inserted = 0
81+
total_updated = 0
82+
total_segments = 0
83+
84+
for playlist_str, versions in payload.items():
85+
try:
86+
playlist_id = int(playlist_str)
87+
except ValueError:
88+
raise SystemExit(f"Playlist key must be an integer, got: {playlist_str!r}")
89+
90+
if not isinstance(versions, dict):
91+
raise SystemExit(
92+
f"Value for playlist {playlist_id} must be an object keyed by version_id."
93+
)
94+
95+
for version_str, version_data in versions.items():
96+
try:
97+
version_id = int(version_str)
98+
except ValueError:
99+
raise SystemExit(
100+
f"Version key under playlist {playlist_id} must be an integer, "
101+
f"got: {version_str!r}"
102+
)
103+
104+
if not isinstance(version_data, dict):
105+
raise SystemExit(
106+
f"Value for playlist {playlist_id} / version {version_id} "
107+
"must be a JSON object."
108+
)
109+
110+
segments = version_data.get("segments")
111+
if not isinstance(segments, list):
112+
raise SystemExit(
113+
f"playlist {playlist_id} / version {version_id}: "
114+
"'segments' must be a list."
115+
)
116+
117+
for i, seg in enumerate(segments):
118+
_validate_segment(seg, i)
119+
120+
print(
121+
f"Playlist {playlist_id} / Version {version_id}: "
122+
f"{len(segments)} segment(s)",
123+
end="",
124+
)
125+
126+
if dry_run:
127+
print(" [dry-run, skipped]")
128+
total_segments += len(segments)
129+
continue
130+
131+
inserted = 0
132+
updated = 0
133+
for seg in segments:
134+
seg_create = StoredSegmentCreate(**seg)
135+
_, is_new = await storage.upsert_segment(
136+
playlist_id=playlist_id,
137+
version_id=version_id,
138+
segment_id=seg_create.segment_id,
139+
data=seg_create,
140+
)
141+
if is_new:
142+
inserted += 1
143+
else:
144+
updated += 1
145+
146+
print(f" → inserted={inserted}, updated={updated}")
147+
total_inserted += inserted
148+
total_updated += updated
149+
total_segments += len(segments)
150+
151+
if dry_run:
152+
print(f"\nDry run complete. {total_segments} segment(s) validated.")
153+
else:
154+
print(
155+
f"\nDone. {total_segments} segment(s) processed: "
156+
f"{total_inserted} inserted, {total_updated} updated."
157+
)
158+
159+
160+
def main() -> None:
161+
parser = argparse.ArgumentParser(
162+
description="Inject transcript segments directly into DNA storage."
163+
)
164+
parser.add_argument(
165+
"json_file",
166+
help="Path to the JSON file containing transcript data to inject",
167+
)
168+
parser.add_argument(
169+
"--dry-run",
170+
action="store_true",
171+
help="Parse and validate the JSON without writing to the database",
172+
)
173+
args = parser.parse_args()
174+
175+
try:
176+
with open(args.json_file, encoding="utf-8") as f:
177+
payload: dict = json.load(f)
178+
except FileNotFoundError:
179+
raise SystemExit(f"File not found: {args.json_file}")
180+
except json.JSONDecodeError as e:
181+
raise SystemExit(f"Invalid JSON in {args.json_file}: {e}")
182+
183+
if not isinstance(payload, dict):
184+
raise SystemExit("Top-level JSON must be an object keyed by playlist_id.")
185+
186+
asyncio.run(_inject(payload, dry_run=args.dry_run))
187+
188+
189+
if __name__ == "__main__":
190+
main()
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"375": {
3+
"7056": {
4+
"segments": [
5+
{
6+
"segment_id": "abc123:speaker-0:1",
7+
"text": "Yeah, this shot is looking really really good. I am not a huge fan of that flare on the right that is intersecting the balloon. Lets see if we can get rid of it.",
8+
"speaker": "James Spadafora",
9+
"language": "en",
10+
"start_time": 0.0,
11+
"end_time": 30.2,
12+
"absolute_start_time": "2026-06-22T10:00:00.000Z",
13+
"absolute_end_time": "2026-06-22T10:00:30.200Z",
14+
"completed": true
15+
}
16+
]
17+
},
18+
"7057": {
19+
"segments": [
20+
{
21+
"segment_id": "abc123:speaker-0:1",
22+
"text": "The background on this shot is definitely way too dark. Let's see if we can add a little bit more lighting to the background while still keeping the moody feel. Also, let's let's go back to modeling and bump up that table a little bit, make it a bit bigger. Make sure you check in with Olga on the texturing. great next version.",
23+
"speaker": "James Spadafora",
24+
"language": "en",
25+
"start_time": 0.0,
26+
"end_time": 30.2,
27+
"absolute_start_time": "2026-06-22T10:00:00.000Z",
28+
"absolute_end_time": "2026-06-22T10:00:30.200Z",
29+
"completed": true
30+
}
31+
]
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)