Kacper Łukawski commited on
Commit
804c481
·
1 Parent(s): de1cc73

Add download script

Browse files
Files changed (4) hide show
  1. README.md +22 -0
  2. pyproject.toml +7 -0
  3. scripts/download_transcripts.py +174 -0
  4. uv.lock +102 -0
README.md CHANGED
@@ -28,6 +28,28 @@ uv sync
28
  uv run main.py
29
  ```
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  ## Running with Docker
32
 
33
  ```bash
 
28
  uv run main.py
29
  ```
30
 
31
+ ## Helper scripts
32
+
33
+ ### Download YouTube transcripts
34
+
35
+ Downloads transcripts for one or more YouTube videos and saves each as a `.docx` file named after the video title. Prefers manually-created transcriptions; falls back to auto-generated if unavailable.
36
+
37
+ ```bash
38
+ # Single video
39
+ uv run scripts/download_transcripts.py https://www.youtube.com/watch?v=VIDEO_ID
40
+
41
+ # Multiple videos, custom output directory
42
+ uv run scripts/download_transcripts.py \
43
+ https://youtu.be/VIDEO_ID1 \
44
+ https://youtu.be/VIDEO_ID2 \
45
+ -o ./my-transcripts
46
+
47
+ # Verbose logging
48
+ uv run scripts/download_transcripts.py https://youtu.be/VIDEO_ID -v
49
+ ```
50
+
51
+ Output files land in `./transcripts/` by default. Each file is named after the video title (e.g. `My_Talk_Title.docx`). Exit code is non-zero if any video failed.
52
+
53
  ## Running with Docker
54
 
55
  ```bash
pyproject.toml CHANGED
@@ -8,3 +8,10 @@ dependencies = [
8
  "mcp-haystack>=1.3.0",
9
  "nvidia-haystack>=1.1.0",
10
  ]
 
 
 
 
 
 
 
 
8
  "mcp-haystack>=1.3.0",
9
  "nvidia-haystack>=1.1.0",
10
  ]
11
+
12
+ [dependency-groups]
13
+ dev = [
14
+ "python-docx>=1.1.2",
15
+ "youtube-transcript-api>=1.2.4",
16
+ "yt-dlp>=2026.3.17",
17
+ ]
scripts/download_transcripts.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Download transcripts from YouTube videos and save them as text files."""
3
+
4
+ import argparse
5
+ import logging
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+ from urllib.parse import parse_qs, urlparse
10
+
11
+ import yt_dlp
12
+ from docx import Document
13
+ from youtube_transcript_api import (
14
+ NoTranscriptFound,
15
+ TranscriptsDisabled,
16
+ VideoUnavailable,
17
+ YouTubeTranscriptApi,
18
+ )
19
+
20
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _ENGLISH_CODES = ["en", "en-US", "en-GB"]
24
+
25
+
26
+ def parse_args() -> argparse.Namespace:
27
+ parser = argparse.ArgumentParser(description="Download YouTube video transcripts to text files.")
28
+ parser.add_argument("videos", nargs="+", metavar="URL_OR_ID", help="YouTube URLs or video IDs")
29
+ parser.add_argument("-o", "--output", type=Path, default=Path("transcripts"), metavar="DIR")
30
+ parser.add_argument("-v", "--verbose", action="store_true")
31
+ return parser.parse_args()
32
+
33
+
34
+ def extract_video_id(url_or_id: str) -> str:
35
+ parsed = urlparse(url_or_id)
36
+ if parsed.scheme in ("http", "https"):
37
+ host = parsed.netloc.lower().lstrip("www.")
38
+ if host == "youtu.be":
39
+ vid = parsed.path.lstrip("/").split("/")[0]
40
+ elif host in ("youtube.com", "m.youtube.com"):
41
+ if parsed.path.startswith("/shorts/"):
42
+ vid = parsed.path.split("/shorts/")[1].split("/")[0]
43
+ else:
44
+ qs = parse_qs(parsed.query)
45
+ candidates = qs.get("v", [])
46
+ if not candidates:
47
+ raise ValueError(f"No video ID in URL: {url_or_id}")
48
+ vid = candidates[0]
49
+ else:
50
+ raise ValueError(f"Unrecognised YouTube host: {parsed.netloc}")
51
+ vid = vid.split("&")[0].split("?")[0]
52
+ else:
53
+ vid = url_or_id.strip()
54
+
55
+ if not re.fullmatch(r"[a-zA-Z0-9_-]{11}", vid):
56
+ raise ValueError(f"Does not look like a video ID: {vid!r}")
57
+ return vid
58
+
59
+
60
+ def fetch_title(video_id: str) -> str:
61
+ opts = {
62
+ "quiet": True,
63
+ "no_warnings": True,
64
+ "extract_flat": True,
65
+ "skip_download": True,
66
+ }
67
+ with yt_dlp.YoutubeDL(opts) as ydl:
68
+ info = ydl.extract_info(f"https://www.youtube.com/watch?v={video_id}", download=False)
69
+ if not info or "title" not in info:
70
+ raise RuntimeError("yt-dlp returned no title")
71
+ return info["title"]
72
+
73
+
74
+ def fetch_transcript(video_id: str) -> str:
75
+ transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
76
+
77
+ transcript = None
78
+ try:
79
+ transcript = transcript_list.find_manually_created_transcript(_ENGLISH_CODES)
80
+ logger.debug("Using manually-created English transcript for %s", video_id)
81
+ except NoTranscriptFound:
82
+ pass
83
+
84
+ if transcript is None:
85
+ try:
86
+ transcript = transcript_list.find_generated_transcript(_ENGLISH_CODES)
87
+ logger.debug("Using auto-generated English transcript for %s", video_id)
88
+ except NoTranscriptFound:
89
+ pass
90
+
91
+ if transcript is None:
92
+ available = list(transcript_list)
93
+ if not available:
94
+ raise NoTranscriptFound(video_id, [])
95
+ transcript = available[0]
96
+ logger.warning(
97
+ "No English transcript for %s; using language %s", video_id, transcript.language_code
98
+ )
99
+
100
+ entries = transcript.fetch()
101
+ return " ".join(entry["text"] for entry in entries)
102
+
103
+
104
+ def sanitize_filename(title: str, video_id: str) -> str:
105
+ name = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", title)
106
+ name = re.sub(r"\s+", "_", name)
107
+ name = re.sub(r"_+", "_", name)
108
+ name = name.strip("_.")
109
+ name = name[:200]
110
+ return name if name else video_id
111
+
112
+
113
+ def save_transcript(text: str, stem: str, output_dir: Path) -> Path:
114
+ output_dir.mkdir(parents=True, exist_ok=True)
115
+ path = output_dir / f"{stem}.docx"
116
+ doc = Document()
117
+ doc.add_paragraph(text)
118
+ doc.save(path)
119
+ return path
120
+
121
+
122
+ def process_video(url_or_id: str, output_dir: Path) -> bool:
123
+ video_id: str | None = None
124
+ try:
125
+ video_id = extract_video_id(url_or_id)
126
+ except ValueError as exc:
127
+ logger.error("Could not parse video ID from %r: %s", url_or_id, exc)
128
+ return False
129
+
130
+ title_stem = video_id
131
+ try:
132
+ title_stem = sanitize_filename(fetch_title(video_id), video_id)
133
+ except Exception as exc:
134
+ logger.warning("Could not fetch title for %s: %s — using video ID as filename", video_id, exc)
135
+
136
+ try:
137
+ text = fetch_transcript(video_id)
138
+ except TranscriptsDisabled:
139
+ logger.error("Transcripts are disabled for %s", video_id)
140
+ return False
141
+ except VideoUnavailable:
142
+ logger.error("Video unavailable: %s", video_id)
143
+ return False
144
+ except NoTranscriptFound:
145
+ logger.error("No transcript available for %s", video_id)
146
+ return False
147
+ except Exception as exc:
148
+ logger.error("Unexpected error fetching transcript for %s: %s", video_id, exc)
149
+ return False
150
+
151
+ try:
152
+ path = save_transcript(text, title_stem, output_dir)
153
+ logger.info("Saved: %s", path)
154
+ return True
155
+ except Exception as exc:
156
+ logger.error("Could not write file for %s: %s", video_id, exc)
157
+ return False
158
+
159
+
160
+ def main() -> None:
161
+ args = parse_args()
162
+ if args.verbose:
163
+ logging.getLogger().setLevel(logging.DEBUG)
164
+
165
+ results = [process_video(v, args.output) for v in args.videos]
166
+ success = sum(results)
167
+ total = len(results)
168
+ logger.info("%d/%d transcripts downloaded", success, total)
169
+ if success < total:
170
+ sys.exit(1)
171
+
172
+
173
+ if __name__ == "__main__":
174
+ main()
uv.lock CHANGED
@@ -207,6 +207,15 @@ wheels = [
207
  { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
208
  ]
209
 
 
 
 
 
 
 
 
 
 
210
  [[package]]
211
  name = "distro"
212
  version = "1.9.0"
@@ -567,6 +576,50 @@ wheels = [
567
  { url = "https://files.pythonhosted.org/packages/cd/62/60ed24fa8707f10c1c5aef94791252b820be3dd6bdfc6e2fcdb08bc8912f/lazy_imports-1.2.0-py3-none-any.whl", hash = "sha256:97134d6552e2ba16f1a278e316f05313ab73b360e848e40d593d08a5c2406fdf", size = 18681, upload-time = "2025-12-28T13:51:49.802Z" },
568
  ]
569
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  [[package]]
571
  name = "markdown-it-py"
572
  version = "4.0.0"
@@ -749,6 +802,13 @@ dependencies = [
749
  { name = "nvidia-haystack" },
750
  ]
751
 
 
 
 
 
 
 
 
752
  [package.metadata]
753
  requires-dist = [
754
  { name = "fastapi", extras = ["standard"], specifier = ">=0.134.0" },
@@ -757,6 +817,13 @@ requires-dist = [
757
  { name = "nvidia-haystack", specifier = ">=1.1.0" },
758
  ]
759
 
 
 
 
 
 
 
 
760
  [[package]]
761
  name = "posthog"
762
  version = "7.11.1"
@@ -906,6 +973,19 @@ wheels = [
906
  { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
907
  ]
908
 
 
 
 
 
 
 
 
 
 
 
 
 
 
909
  [[package]]
910
  name = "python-dotenv"
911
  version = "1.2.2"
@@ -1325,3 +1405,25 @@ wheels = [
1325
  { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
1326
  { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
1327
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
208
  ]
209
 
210
+ [[package]]
211
+ name = "defusedxml"
212
+ version = "0.7.1"
213
+ source = { registry = "https://pypi.org/simple" }
214
+ sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
215
+ wheels = [
216
+ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
217
+ ]
218
+
219
  [[package]]
220
  name = "distro"
221
  version = "1.9.0"
 
576
  { url = "https://files.pythonhosted.org/packages/cd/62/60ed24fa8707f10c1c5aef94791252b820be3dd6bdfc6e2fcdb08bc8912f/lazy_imports-1.2.0-py3-none-any.whl", hash = "sha256:97134d6552e2ba16f1a278e316f05313ab73b360e848e40d593d08a5c2406fdf", size = 18681, upload-time = "2025-12-28T13:51:49.802Z" },
577
  ]
578
 
579
+ [[package]]
580
+ name = "lxml"
581
+ version = "6.1.0"
582
+ source = { registry = "https://pypi.org/simple" }
583
+ sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" }
584
+ wheels = [
585
+ { url = "https://files.pythonhosted.org/packages/eb/45/cee4cf203ef0bab5c52afc118da61d6b460c928f2893d40023cfa27e0b80/lxml-6.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8", size = 8576713, upload-time = "2026-04-18T04:32:06.831Z" },
586
+ { url = "https://files.pythonhosted.org/packages/8a/a7/eda05babeb7e046839204eaf254cd4d7c9130ce2bbf0d9e90ea41af5654d/lxml-6.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9", size = 4623874, upload-time = "2026-04-18T04:32:10.755Z" },
587
+ { url = "https://files.pythonhosted.org/packages/e7/e9/db5846de9b436b91890a62f29d80cd849ea17948a49bf532d5278ee69a9e/lxml-6.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03", size = 4949535, upload-time = "2026-04-18T04:34:06.657Z" },
588
+ { url = "https://files.pythonhosted.org/packages/5a/ba/0d3593373dcae1d68f40dc3c41a5a92f2544e68115eb2f62319a4c2a6500/lxml-6.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb", size = 5086881, upload-time = "2026-04-18T04:34:09.556Z" },
589
+ { url = "https://files.pythonhosted.org/packages/43/76/759a7484539ad1af0d125a9afe9c3fb5f82a8779fd1f5f56319d9e4ea2fd/lxml-6.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c", size = 5031305, upload-time = "2026-04-18T04:34:12.336Z" },
590
+ { url = "https://files.pythonhosted.org/packages/dc/b9/c1f0daf981a11e47636126901fd4ab82429e18c57aeb0fc3ad2940b42d8b/lxml-6.1.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28", size = 5647522, upload-time = "2026-04-18T04:34:14.89Z" },
591
+ { url = "https://files.pythonhosted.org/packages/31/e6/1f533dcd205275363d9ba3511bcec52fa2df86abf8abe6a5f2c599f0dc31/lxml-6.1.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086", size = 5239310, upload-time = "2026-04-18T04:34:17.652Z" },
592
+ { url = "https://files.pythonhosted.org/packages/c3/8c/4175fb709c78a6e315ed814ed33be3defd8b8721067e70419a6cf6f971da/lxml-6.1.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f", size = 5350799, upload-time = "2026-04-18T04:34:20.529Z" },
593
+ { url = "https://files.pythonhosted.org/packages/fd/77/6ffdebc5994975f0dde4acb59761902bd9d9bb84422b9a0bd239a7da9ca8/lxml-6.1.0-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292", size = 4697693, upload-time = "2026-04-18T04:34:23.541Z" },
594
+ { url = "https://files.pythonhosted.org/packages/f8/f1/565f36bd5c73294602d48e04d23f81ff4c8736be6ba5e1d1ec670ac9be80/lxml-6.1.0-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb", size = 5250708, upload-time = "2026-04-18T04:34:26.001Z" },
595
+ { url = "https://files.pythonhosted.org/packages/5a/11/a68ab9dd18c5c499404deb4005f4bc4e0e88e5b72cd755ad96efec81d18d/lxml-6.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad", size = 5084737, upload-time = "2026-04-18T04:34:28.32Z" },
596
+ { url = "https://files.pythonhosted.org/packages/ab/78/e8f41e2c74f4af564e6a0348aea69fb6daaefa64bc071ef469823d22cc18/lxml-6.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb", size = 4737817, upload-time = "2026-04-18T04:34:30.784Z" },
597
+ { url = "https://files.pythonhosted.org/packages/06/2d/aa4e117aa2ce2f3b35d9ff246be74a2f8e853baba5d2a92c64744474603a/lxml-6.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f", size = 5670753, upload-time = "2026-04-18T04:34:33.675Z" },
598
+ { url = "https://files.pythonhosted.org/packages/08/f5/dd745d50c0409031dbfcc4881740542a01e54d6f0110bd420fa7782110b8/lxml-6.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43", size = 5238071, upload-time = "2026-04-18T04:34:36.12Z" },
599
+ { url = "https://files.pythonhosted.org/packages/3e/74/ad424f36d0340a904665867dab310a3f1f4c96ff4039698de83b77f44c1f/lxml-6.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585", size = 5264319, upload-time = "2026-04-18T04:34:39.035Z" },
600
+ { url = "https://files.pythonhosted.org/packages/53/36/a15d8b3514ec889bfd6aa3609107fcb6c9189f8dc347f1c0b81eded8d87c/lxml-6.1.0-cp314-cp314-win32.whl", hash = "sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f", size = 3657139, upload-time = "2026-04-18T04:32:20.006Z" },
601
+ { url = "https://files.pythonhosted.org/packages/1a/a4/263ebb0710851a3c6c937180a9a86df1206fdfe53cc43005aa2237fd7736/lxml-6.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120", size = 4064195, upload-time = "2026-04-18T04:32:23.876Z" },
602
+ { url = "https://files.pythonhosted.org/packages/80/68/2000f29d323b6c286de077ad20b429fc52272e44eae6d295467043e56012/lxml-6.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946", size = 3741870, upload-time = "2026-04-18T04:32:27.922Z" },
603
+ { url = "https://files.pythonhosted.org/packages/30/e9/21383c7c8d43799f0da90224c0d7c921870d476ec9b3e01e1b2c0b8237c5/lxml-6.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c", size = 8827548, upload-time = "2026-04-18T04:32:15.094Z" },
604
+ { url = "https://files.pythonhosted.org/packages/a5/01/c6bc11cd587030dd4f719f65c5657960649fe3e19196c844c75bf32cd0d6/lxml-6.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d", size = 4735866, upload-time = "2026-04-18T04:32:18.924Z" },
605
+ { url = "https://files.pythonhosted.org/packages/f3/01/757132fff5f4acf25463b5298f1a46099f3a94480b806547b29ce5e385de/lxml-6.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9", size = 4969476, upload-time = "2026-04-18T04:34:41.889Z" },
606
+ { url = "https://files.pythonhosted.org/packages/fd/fb/1bc8b9d27ed64be7c8903db6c89e74dc8c2cd9ec630a7462e4654316dc5b/lxml-6.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9", size = 5103719, upload-time = "2026-04-18T04:34:44.797Z" },
607
+ { url = "https://files.pythonhosted.org/packages/d5/e7/5bf82fa28133536a54601aae633b14988e89ed61d4c1eb6b899b023233aa/lxml-6.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7", size = 5027890, upload-time = "2026-04-18T04:34:47.634Z" },
608
+ { url = "https://files.pythonhosted.org/packages/2d/20/e048db5d4b4ea0366648aa595f26bb764b2670903fc585b87436d0a5032c/lxml-6.1.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86", size = 5596008, upload-time = "2026-04-18T04:34:51.503Z" },
609
+ { url = "https://files.pythonhosted.org/packages/9a/c2/d10807bc8da4824b39e5bd01b5d05c077b6fd01bd91584167edf6b269d22/lxml-6.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb", size = 5224451, upload-time = "2026-04-18T04:34:54.263Z" },
610
+ { url = "https://files.pythonhosted.org/packages/3c/15/2ebea45bea427e7f0057e9ce7b2d62c5aba20c6b001cca89ed0aadb3ad41/lxml-6.1.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c", size = 5312135, upload-time = "2026-04-18T04:34:56.818Z" },
611
+ { url = "https://files.pythonhosted.org/packages/31/e2/87eeae151b0be2a308d49a7ec444ff3eb192b14251e62addb29d0bf3778f/lxml-6.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f", size = 4639126, upload-time = "2026-04-18T04:34:59.704Z" },
612
+ { url = "https://files.pythonhosted.org/packages/a3/51/8a3f6a20902ad604dd746ec7b4000311b240d389dac5e9d95adefd349e0c/lxml-6.1.0-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773", size = 5232579, upload-time = "2026-04-18T04:35:02.658Z" },
613
+ { url = "https://files.pythonhosted.org/packages/6d/d2/650d619bdbe048d2c3f2c31edb00e35670a5e2d65b4fe3b61bce37b19121/lxml-6.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b", size = 5084206, upload-time = "2026-04-18T04:35:05.175Z" },
614
+ { url = "https://files.pythonhosted.org/packages/dd/8a/672ca1a3cbeabd1f511ca275a916c0514b747f4b85bdaae103b8fa92f307/lxml-6.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405", size = 4758906, upload-time = "2026-04-18T04:35:08.098Z" },
615
+ { url = "https://files.pythonhosted.org/packages/be/f1/ef4b691da85c916cb2feb1eec7414f678162798ac85e042fa164419ac05c/lxml-6.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690", size = 5620553, upload-time = "2026-04-18T04:35:11.23Z" },
616
+ { url = "https://files.pythonhosted.org/packages/59/17/94e81def74107809755ac2782fdad4404420f1c92ca83433d117a6d5acf0/lxml-6.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd", size = 5229458, upload-time = "2026-04-18T04:35:14.254Z" },
617
+ { url = "https://files.pythonhosted.org/packages/21/55/c4be91b0f830a871fc1b0d730943d56013b683d4671d5198260e2eae722b/lxml-6.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180", size = 5247861, upload-time = "2026-04-18T04:35:17.006Z" },
618
+ { url = "https://files.pythonhosted.org/packages/c2/ca/77123e4d77df3cb1e968ade7b1f808f5d3a5c1c96b18a33895397de292c1/lxml-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2", size = 3897377, upload-time = "2026-04-18T04:32:07.656Z" },
619
+ { url = "https://files.pythonhosted.org/packages/64/ce/3554833989d074267c063209bae8b09815e5656456a2d332b947806b05ff/lxml-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5", size = 4392701, upload-time = "2026-04-18T04:32:12.113Z" },
620
+ { url = "https://files.pythonhosted.org/packages/2b/a0/9b916c68c0e57752c07f8f64b30138d9d4059dbeb27b90274dedbea128ff/lxml-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac", size = 3817120, upload-time = "2026-04-18T04:32:15.803Z" },
621
+ ]
622
+
623
  [[package]]
624
  name = "markdown-it-py"
625
  version = "4.0.0"
 
802
  { name = "nvidia-haystack" },
803
  ]
804
 
805
+ [package.dev-dependencies]
806
+ dev = [
807
+ { name = "python-docx" },
808
+ { name = "youtube-transcript-api" },
809
+ { name = "yt-dlp" },
810
+ ]
811
+
812
  [package.metadata]
813
  requires-dist = [
814
  { name = "fastapi", extras = ["standard"], specifier = ">=0.134.0" },
 
817
  { name = "nvidia-haystack", specifier = ">=1.1.0" },
818
  ]
819
 
820
+ [package.metadata.requires-dev]
821
+ dev = [
822
+ { name = "python-docx", specifier = ">=1.1.2" },
823
+ { name = "youtube-transcript-api", specifier = ">=1.2.4" },
824
+ { name = "yt-dlp", specifier = ">=2026.3.17" },
825
+ ]
826
+
827
  [[package]]
828
  name = "posthog"
829
  version = "7.11.1"
 
973
  { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
974
  ]
975
 
976
+ [[package]]
977
+ name = "python-docx"
978
+ version = "1.2.0"
979
+ source = { registry = "https://pypi.org/simple" }
980
+ dependencies = [
981
+ { name = "lxml" },
982
+ { name = "typing-extensions" },
983
+ ]
984
+ sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" }
985
+ wheels = [
986
+ { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" },
987
+ ]
988
+
989
  [[package]]
990
  name = "python-dotenv"
991
  version = "1.2.2"
 
1405
  { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
1406
  { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
1407
  ]
1408
+
1409
+ [[package]]
1410
+ name = "youtube-transcript-api"
1411
+ version = "1.2.4"
1412
+ source = { registry = "https://pypi.org/simple" }
1413
+ dependencies = [
1414
+ { name = "defusedxml" },
1415
+ { name = "requests" },
1416
+ ]
1417
+ sdist = { url = "https://files.pythonhosted.org/packages/60/43/4104185a2eaa839daa693b30e15c37e7e58795e8e09ec414f22b3db54bec/youtube_transcript_api-1.2.4.tar.gz", hash = "sha256:b72d0e96a335df599d67cee51d49e143cff4f45b84bcafc202ff51291603ddcd", size = 469839, upload-time = "2026-01-29T09:09:17.088Z" }
1418
+ wheels = [
1419
+ { url = "https://files.pythonhosted.org/packages/be/95/129ea37efd6cd6ed00f62baae6543345c677810b8a3bf0026756e1d3cf3c/youtube_transcript_api-1.2.4-py3-none-any.whl", hash = "sha256:03878759356da5caf5edac77431780b91448fb3d8c21d4496015bdc8a7bc43ff", size = 485227, upload-time = "2026-01-29T09:09:15.427Z" },
1420
+ ]
1421
+
1422
+ [[package]]
1423
+ name = "yt-dlp"
1424
+ version = "2026.3.17"
1425
+ source = { registry = "https://pypi.org/simple" }
1426
+ sdist = { url = "https://files.pythonhosted.org/packages/8b/34/7c6b4e3f89cb6416d2cd7ab6dab141a1df97ab0fb22d15816db2c92148c9/yt_dlp-2026.3.17.tar.gz", hash = "sha256:ba7aa31d533f1ffccfe70e421596d7ca8ff0bf1398dc6bb658b7d9dec057d2c9", size = 3119221, upload-time = "2026-03-17T23:43:00.244Z" }
1427
+ wheels = [
1428
+ { url = "https://files.pythonhosted.org/packages/cd/13/5093bcb954878e50f7217fd2ab94282b53934022e4e4a03265582da83bf5/yt_dlp-2026.3.17-py3-none-any.whl", hash = "sha256:32992db94303a8a5d211a183f2174834fe7f8c29d83ed2e7a324eae97a8f26d8", size = 3315134, upload-time = "2026-03-17T23:42:57.863Z" },
1429
+ ]