The multimodal ingestion pipeline was audited against all required modalities defined in the Week 7/8 task specification. Every modality was tested using real-world datasets (not synthetic data).
| Modality | File Types | Test Dataset | Status |
|---|---|---|---|
| Text (digital) | TXT, MD, HTML | WikiText-103 (3 files) | PASS |
| UnstructuredPDFLoader (via LangChain) | Supported (no PDF test data in current dataset) | ||
| Word | DOCX | UnstructuredWordDocumentLoader | Supported (no DOCX test data) |
| Slides | PPTX | UnstructuredPowerPointLoader | Supported (no PPTX test data) |
| Images (OCR) | JPG, PNG, TIF, BMP | CORD-v2 receipts (50 images) | PASS (107/150 produced text) |
| Audio (ASR) | MP3, WAV, FLAC, M4A | LibriSpeech (2 FLAC files) | PASS |
| Video | MP4, WEBM, MOV, AVI | YouTube UGC subset (4 WEBM files) | PASS (chunked + keyframes) |
| CSV | CSV | MTSamples medical corpus (4999 rows) | PASS |
Final test suite output (9/9 pass):
[PASS] text 3 files -> 3 records, 0 errors
[PASS] image 5 files -> 3 records, 2 empty OCR
[PASS] audio 2 files -> 2 records, 0 errors
[PASS] video 2 files -> 34 records, 0 errors
[PASS] csv 1 files -> 4999 records, 0 errors
[PASS] mixed 15 files -> 22 records, 0 errors
[PASS] max_files caps 1,2,3 respected; uncapped processed 3
[PASS] reporting succeeded=3, failed=0, empty=2
[PASS] schema All 5072 records pass schema validation
Cause: The default Python 3.14 venv cannot install faster-whisper (requires Python <3.13). When the batch run pointed at 00_Data_test, it encountered video files first and all failed with a dependency error. The cleaned_documents.jsonl was 0 bytes.
Fix: Documented that venv/bin/python3.11 must be used. The test suite auto-detects missing dependencies and SKIPs rather than FAILs.
Cause: When records_from_image() returned [] (OCR produced no text), the file was silently skipped — no logging, no tracking. Same for text documents that produced empty content after normalization.
Fix: Empty extraction is now explicitly tracked. The ingest() function records these in empty_files with modality and reason, and logs them to ingestion_errors.jsonl with "stage": "empty_content".
Cause: The log_error() function only recorded {path, stage, error} — no indication of what type of file failed.
Fix: Error log entries now include modality and filetype fields:
{"path": "video/clip.mp4", "stage": "ingest", "error": "Missing faster-whisper", "modality": "video", "filetype": "video"}
Cause: ingestion_stats.json only reported aggregate counts (files_processed, records_emitted, errors). No way to see which files succeeded vs. failed.
Fix: Stats now include explicit file-level tracking:
{
"counts": {
"files_processed": 5,
"files_succeeded": 3,
"files_failed": 0,
"files_empty": 2,
"files_skipped": 0,
"records_emitted": 3,
"errors": 2
},
"success_files": ["train_000037.png", "test_000020.png", "test_000008.png"],
"failed_files": [],
"empty_files": [
{"path": "test_000034.png", "modality": "image", "reason": "Extraction produced no content"},
{"path": "train_000023.png", "modality": "image", "reason": "Extraction produced no content"}
]
}
iter_files() output directory exclusion too narrowCause: The original filter if "01_data_ingestion" in path.parts and "output" in path.parts only excluded files under 01_data_ingestion/output/, not output directories in arbitrary locations.
Fix: Simplified to if "output" in path.parts which prevents re-ingesting any previous output.
| Fix | File | Description |
|---|---|---|
| Enhanced error logging | ingest_multimodal.py |
log_error() now accepts and includes modality and filetype |
| Success/fail/empty tracking | ingest_multimodal.py |
New lists: success_files, failed_files, empty_files, skipped_files |
| Silent failure logging | ingest_multimodal.py |
Empty extraction now logged as "stage": "empty_content" |
| Skipped file logging | ingest_multimodal.py |
Unknown file types tracked in skipped_files |
| Same reporting in folder script | ingest_folder.py |
Mirrored all reporting enhancements |
| Reporting tests | test_ingestion.py |
New test_reporting validates stats structure |
| Output dir filter fix | ingest_multimodal.py |
Broader output directory exclusion |
| Automated test suite | test_ingestion.py |
12 tests covering all modalities + schema + reporting + chunked video + keyframes |
| Batch folder script | ingest_folder.py |
--modality and --filetype filtering for targeted ingestion |
| Chunked video processing | ingest_multimodal.py |
Time-based audio chunking with checkpoint recovery for long videos |
| Keyframe extraction | ingest_multimodal.py |
FFmpeg scene detection + OCR for visual content in videos |
| Video CLI flags | both scripts | --chunk-duration, --extract-keyframes, --scene-threshold |
All modalities produce records conforming to this schema:
{
"doc_id": "policies-handbook-2024-pdf__a1b2c3d4",
"page": 4,
"section": "page_4",
"section_index": 7,
"content": "Employees are entitled to parental leave...",
"source": {
"path": "policies/handbook_2024.pdf",
"filename": "handbook_2024.pdf",
"filetype": "pdf",
"modality": "text",
"loader": "UnstructuredPDFLoader",
"record_id": "policies-handbook-2024-pdf__a1b2c3d4:000007",
"prev_record_id": "policies-handbook-2024-pdf__a1b2c3d4:000006",
"next_record_id": "policies-handbook-2024-pdf__a1b2c3d4:000008"
}
}
Modality-specific metadata fields:
| Field | Modalities | Description |
|---|---|---|
source.page_number |
text (PDF, DOCX, PPTX) | Page or slide number |
source.page_type |
text (PDF, DOCX, PPTX) | "page" or "slide" |
source.element_type |
text (Unstructured) | Element category (e.g., "NarrativeText") |
source.timestamp_start |
audio, video | Segment start time in seconds |
source.timestamp_end |
audio, video | Segment end time in seconds |
source.timestamp |
audio, video | Formatted as HH:MM:SS.mmm |
source.language |
audio, video | Detected language code |
source.language_probability |
audio, video | Detection confidence (0-1) |
source.ocr_engine |
image, video_frame | "tesseract" or "paddle" |
source.ocr_language |
image, video_frame | Language code used for OCR |
source.row_index |
csv | Row number (1-indexed) |
source.column_count |
csv | Number of columns |
source.frame_path |
video_frame | Relative path to extracted frame PNG |
source.frame_index |
video_frame | Sequential index of extracted frame |
source.scene_threshold |
video_frame | Scene detection threshold used |
Mapping to example schema from instructions:
| Instruction Example | Our Schema | Notes |
|---|---|---|
doc_id |
doc_id |
Identical |
source_type |
source.modality + source.filetype |
Split into two fields for precision |
location.timestamp |
source.timestamp |
Flat field, same HH:MM:SS format |
content |
content |
Identical |
metadata |
source |
All metadata consolidated in source dict |
The schema is consumed as-is by the downstream chunk_documents.py (on main) and chunk_mtsamples.py pipelines.
Legal hearings dataset validated — Nathan's dataset (Zuckerberg Congressional Hearing 2018 MP4 + Theranos trial exhibit PDFs) has been ingested successfully. The 5h15m video uses checkpoint-based caching with 300-second chunks. All PDFs processed without errors.
LangSmith not integrated into ingestion — LangSmith tracing is configured on main for retrieval (04_vectoredb/config.py) but not for ingestion. Adding @traceable decorators to ingestion functions would enable end-to-end observability.
Single-region OCR per image — Each image produces exactly one record. No spatial/bounding-box indexing for multi-region documents.
Python version constraint — Audio/video ingestion requires Python 3.10-3.12 due to onnxruntime dependency in faster-whisper. The project's default venv uses Python 3.14 which silently skips the dependency.
No PDF-specific test data — PDF ingestion is supported via UnstructuredPDFLoader with configurable strategy (auto, hi_res, fast, ocr_only), but no PDF files exist in the test dataset. Testing was done only via the loader API.
No DOCX/PPTX test data — Supported via LangChain Unstructured loaders but untested with real files.
Keyframe OCR quality depends on video resolution — Low-resolution videos or videos with text rendered at small sizes may produce poor or empty OCR results. The pipeline skips frames with no OCR output to avoid polluting the index with empty records.
Checkpoint stores segment count only, not records — Checkpoint files track which chunks are complete and the running segment count. On resume, already-completed chunks are skipped and transcription continues from where it left off. For extremely long videos (10+ hours), a future optimization could stream records to an intermediate file.
| Script | Purpose | Usage |
|---|---|---|
ingest_multimodal.py |
Core ingestion engine | python ingest_multimodal.py --input 00_Data --chunk-duration 600 --extract-keyframes |
ingest_folder.py |
Batch ingestion with filtering | python ingest_folder.py --input /path --modality video --extract-keyframes |
test_ingestion.py |
Automated test suite | python test_ingestion.py --data-dir 00_Data_test --modality all |
validate_ingestion.py |
JSONL schema validator | python validate_ingestion.py --input output/cleaned_documents.jsonl |
download_test_datasets.py |
Download test data | python download_test_datasets.py --out 00_Data_test --text --image --audio --video |