Compare commits

..

10 Commits

Author SHA1 Message Date
Mariano Gabriel
ef72d06bfe docs 2026-07-06 02:41:50 -03:00
Mariano Gabriel
31ad8e5928 attempt to use separate folder for docs and meetings 2026-07-06 01:18:17 -03:00
Mariano Gabriel
bb84558526 scan 2026-07-05 22:59:27 -03:00
Mariano Gabriel
8cfb4c2cbe merging 2026-07-05 22:37:46 -03:00
Mariano Gabriel
b65d6075be basic search 2026-07-05 22:00:52 -03:00
Mariano Gabriel
8ff29fc776 embedded meetus 2026-07-05 21:35:09 -03:00
Mariano Gabriel
a92615d6bc embed meetus 2026-07-05 20:52:37 -03:00
Mariano Gabriel
8606520ef2 doocus improvemnts 2026-07-05 20:35:44 -03:00
Mariano Gabriel
7b276e8f3d viewers 2026-07-05 11:26:45 -03:00
Mariano Gabriel
300806b936 doocus update and meeting list export 2026-07-05 11:15:57 -03:00
43 changed files with 3505 additions and 960 deletions

6
.gitignore vendored
View File

@@ -9,6 +9,11 @@ output/*
# doocus document extraction output (textified docs + metadata; local-only)
docs-output/
# Managed collection roots (one subfolder per source; index.json + sidecars).
# doocus-data = document collections, meetus-data = meeting collections.
doocus-data/
meetus-data/
# Python cache
__pycache__
*.pyc
@@ -18,3 +23,4 @@ __pycache__
.venv/
local-run.sh
meetings

View File

@@ -51,9 +51,16 @@ docs-output/ whole tree replicated, no flatt
Node modes: **extracted** (docx/pdf/pptx/xlsx → text sidecar), **meeting**
(mp4/... → belongs to meetus), **link** (everything else → original is the
artifact, not duplicated). Each node has a `url` slot for the eventual Drive link.
`make docs` (→ `process_tree.py`) indexes a whole tree; `process_doc.py` still does
one-off single-file extraction. Full doocus docs: [`doocus/README.md`](doocus/README.md).
artifact, not duplicated). Each node has a `url` slot for the eventual Drive link
and a `created` date. `make docs` (→ `process_tree.py`) indexes a whole tree;
`process_doc.py` still does one-off single-file extraction.
Meetings stay coherent with docs: `docs-output/meetings.txt` lists the meeting
paths, and `make batch … LIST=… OUT_FORMAT="{name}.meetus"` runs meetus into a
`<file>.meetus/` sidecar beside the docs — the exact path the indexer already
pointed each `meeting` node at (`out`/`transcript`). meetus's `--out-format`
(tokens `{date} {run} {stem} {name}`) makes the run-folder name configurable;
default is unchanged. Full doocus docs: [`doocus/README.md`](doocus/README.md).
Cross-search (docs frame + meetings frame, over cached text) is a later phase. 🚧
## Status legend
@@ -96,15 +103,30 @@ ctrl/ control plane / operational scripts
└─ interleave_cht_frames.py whisperx JSON + cht frames/index.json → enhanced.txt
ui/ local browser UIs (Vue 3 + Vite), shared framework
├─ framework/ ✅ shared component/renderer library + design tokens
├─ meetus-app/ ✅ review meeting runs (was ui/app/)
└─ doocus-app/ ✅ browse extracted docs + build packages (Gemini/NotebookLM)
├─ meetus-app/ ✅ review meeting runs (video · transcript · frames; @review source)
└─ doocus-app/ ✅ browse tree · search · package; embeds the meetus review
doocus-data/ meetus-data/ ✅ managed collection roots (gitignored): <root>/<source>/
docs/graphs/ 📄 architecture diagram (.dot → .svg, `make graphs`)
def/ 📄 design/decision notes, in order (the feature history)
README.md 📄 manual for process_meeting.py
README.md 📄 project overview (meetus + doocus) + architecture diagram
INDEX.md 📄 this file
MARIAN.md 📄 genesis brainstorm (explains why the deprecated OCR path exists)
local-run.sh 📄 personal scratch invocations (gitignored)
```
## Collections & UIs
- **Managed roots** (gitignored): `doocus-data/<source>/` (docs) and
`meetus-data/<source>/` (meetings), each `<root>/<source>/` mirroring the source.
`process_tree.py --only {all|docs|meetings}` scopes a collection.
- **doocus-app** discovers both roots (`DOOCUS_DATA` overrides, comma-separated),
**merges collections that share `index.json.root`** into one source, searches the
cached text, and can **scan a folder** (runs `process_tree` via `uv`) from the UI.
- **meetus-app** is a thin shell over `@review` (`ui/meetus-app/src/components/ReviewBody.vue`),
the review composition doocus embeds — meetus stays the single source.
- **Diagram**: `docs/graphs/architecture.dot``docs/graphs/architecture.svg`
(`make graphs`; needs system graphviz). Both committed.
## Notes
- **Default flow** uses `--embed-images` (frames referenced for the LLM to read) +

View File

@@ -9,6 +9,13 @@
# make batch IN="..." EXT="mp4 mov" # limit extensions
# make batch IN="..." FLAGS="--embed-images" # replace the base flags
#
# Meetings flow (process only the videos doocus found, coherent with the docs):
# 1. uv run make docs IN="/mnt/drive" # → docs-output/{index.json, meetings.txt}
# 2. make batch IN="/mnt/drive" OUT=docs-output \ # meetus over just the meetings…
# LIST=docs-output/meetings.txt \ # …only the listed relative paths
# OUT_FORMAT="{name}.meetus" # …into <file>.meetus/ sidecars
# (matches doocus's index pointer, so meetings sit beside the <file>.doocus/ docs.)
#
# IN is required. OUT defaults to IN (write in place); otherwise the input
# folder structure is mirrored under OUT.
@@ -33,6 +40,16 @@ EXT ?=
PYTHON ?=
export PYTHON
# Optional: process only a newline list of RELATIVE paths (re-rooted at IN),
# e.g. the doocus meeting export. Used to run meetus over just the meetings:
# make batch IN="/mnt/drive" OUT=docs-output LIST=docs-output/meetings.txt
LIST ?=
# Optional: run-folder name template forwarded to process_meeting.py --out-format.
# Tokens: {date} {run} {stem} {name}. Default (empty) → "{date}-{run:03d}-{stem}".
# Use "{name}.meetus" so meetings land in a docs-coherent <file>.meetus/ sidecar.
OUT_FORMAT ?=
# doocus (tree indexing) knobs. DOCS_OUT defaults to docs-output; the whole input
# tree is replicated into index.json under it. DOC_EXTRA forwards one-off flags to
# process_tree.py (e.g. --render for pdf page thumbnails, --ocr for scanned pdfs).
@@ -42,14 +59,28 @@ DOC_EXTRA ?=
# Python interpreter for doocus (override PYTHON, else python3 — use `uv run make`).
PY := $(if $(PYTHON),$(PYTHON),python3)
.PHONY: batch dry docs docs-dry help
# Merge a separate output tree into another (e.g. a meetings run done in its own
# folder → docs-output). Both mirror the same source-relative structure, so this
# just unions the sidecars beside the docs. Always dry-run first.
# make merge-dry SRC=meetings-output # preview
# make merge SRC=meetings-output # apply (DST defaults to docs-output)
MERGE_SRC ?=
MERGE_DST ?= docs-output
# Diagrams: graphviz .dot → .svg (both committed). Requires system graphviz
# (apt install graphviz). `make graphs` renders all docs/graphs/*.dot.
DOT_SRC := $(wildcard docs/graphs/*.dot)
SVG_OUT := $(DOT_SRC:.dot=.svg)
.PHONY: batch dry docs docs-dry merge merge-dry graphs help
batch:
@ctrl/batch.sh -i "$(IN)" -o "$(OUT)" $(if $(EXT),-e "$(EXT)") -- \
$(FLAGS) $(if $(AUDIO_LANG),--language $(AUDIO_LANG)) $(EXTRA)
@ctrl/batch.sh -i "$(IN)" -o "$(OUT)" $(if $(EXT),-e "$(EXT)") $(if $(LIST),-l "$(LIST)") -- \
$(FLAGS) $(if $(AUDIO_LANG),--language $(AUDIO_LANG)) \
$(if $(OUT_FORMAT),--out-format "$(OUT_FORMAT)") $(EXTRA)
dry:
@ctrl/batch.sh -i "$(IN)" -o "$(OUT)" $(if $(EXT),-e "$(EXT)") -n
@ctrl/batch.sh -i "$(IN)" -o "$(OUT)" $(if $(EXT),-e "$(EXT)") $(if $(LIST),-l "$(LIST)") -n
# Document indexing (doocus): replicate the whole IN tree into DOCS_OUT/index.json,
# extracting text sidecars only for docx/pdf/pptx/xlsx.
@@ -62,5 +93,23 @@ docs:
docs-dry:
@$(PY) process_tree.py "$(IN)" --output "$(DOCS_OUT)" --dry-run
# index.json/meetings.txt are excluded so a merge of meeting dumps can never
# clobber the canonical index that lives only in docs-output.
MERGE_EXCLUDES = --exclude index.json --exclude meetings.txt
merge:
@test -n "$(MERGE_SRC)" || { echo "ERROR: MERGE_SRC is required" >&2; exit 1; }
@rsync -a $(MERGE_EXCLUDES) "$(MERGE_SRC)/" "$(MERGE_DST)/" && echo "merged $(MERGE_SRC)$(MERGE_DST)"
merge-dry:
@test -n "$(MERGE_SRC)" || { echo "ERROR: MERGE_SRC is required" >&2; exit 1; }
@rsync -avn $(MERGE_EXCLUDES) "$(MERGE_SRC)/" "$(MERGE_DST)/"
docs/graphs/%.svg: docs/graphs/%.dot
dot -Tsvg $< -o $@
graphs: $(SVG_OUT)
@echo "rendered $(words $(SVG_OUT)) svg(s) from $(words $(DOT_SRC)) dot file(s)"
help:
@sed -n '3,14p' Makefile

340
README.md
View File

@@ -1,293 +1,115 @@
# Meeting Processor
# meetus + doocus — a local, offline context extractor
Extract screen content from meeting recordings and merge with Whisper/WhisperX transcripts for better AI summarization.
Turn a downloaded Google Drive (meetings **and** documents) into a **searchable,
browsable, packageable** local knowledge base — then hand focused subsets to the
AI services you're allowed to use (Gemini web, NotebookLM). Every extraction step
is **deterministic and offline**; no cloud AI ever touches the raw source.
## Overview
Two pipelines feed two local UIs over a shared component framework:
This tool enhances meeting transcripts by combining:
- **Audio transcription** (Whisper or WhisperX with speaker diarization)
- **Screen content extraction** via FFmpeg scene detection
- **Frame embedding** for direct LLM analysis
- **meetus** — meeting recordings → enhanced transcripts (WhisperX + screen frames).
- **doocus** — documents → textified content + metadata, indexed into a tree.
The result is a rich, timestamped transcript with embedded screen frames that provides full context for AI summarization.
![Architecture](docs/graphs/architecture.svg)
## Installation
> Diagram source: [`docs/graphs/architecture.dot`](docs/graphs/architecture.dot)
> (regenerate with `make graphs`). Repo map: [`INDEX.md`](INDEX.md).
### 1. System Dependencies
## Install
**FFmpeg** (required for scene detection and frame extraction):
### System dependencies
```bash
# Ubuntu/Debian
sudo apt-get install ffmpeg
# macOS
brew install ffmpeg
sudo apt-get install ffmpeg graphviz # ffmpeg: frames/thumbnails · graphviz: docs diagrams
# optional: tesseract-ocr (doocus --ocr), poppler-utils (pdf page render)
# macOS: brew install ffmpeg graphviz
```
### 2. Python Dependencies (uv)
Dependencies live in `pyproject.toml` as [uv](https://docs.astral.sh/uv/)
feature groups — install only what you need:
### Python (uv feature groups)
Dependencies live in `pyproject.toml` as [uv](https://docs.astral.sh/uv/) groups —
install only what you need:
```bash
uv sync --group meetus # meeting pipeline (frame extraction)
uv sync --group doocus # document extraction (see doocus/README.md)
uv sync --group meetus # meeting pipeline (opencv, ffmpeg-python)
uv sync --group doocus # document extraction (docx/pdf/pptx/xlsx/…)
uv sync --group doocus --group ocr # + OCR for images / scanned pdfs
uv run process_meeting.py samples/meeting.mkv --embed-images --scene-detection --diarize
```
Groups: `meetus`, `doocus`, `ocr`, `pdf-render`, `deprecated`. **whisper/whisperx**
are external CLI tools (like ffmpeg) — install separately (often a GPU env):
`pip install whisperx` (diarization needs a HuggingFace token with pyannote access).
Groups: `meetus`, `doocus`, `ocr`, `pdf-render`, `deprecated` (the last is the
unwired OCR/vision path — the only user of `ollama`, kept out of every default
install).
### 3. Whisper or WhisperX (for audio transcription)
meetus calls these as **external CLI tools** (like ffmpeg), so they are *not*
uv-managed — install them however suits your machine (often a separate GPU env):
### UIs (Node)
```bash
# standard whisper
pip install openai-whisper
# or WhisperX (recommended - adds speaker diarization)
pip install whisperx
cd ui/meetus-app && npm install
cd ui/doocus-app && npm install
```
For speaker diarization, you'll need a HuggingFace token with access to pyannote models.
## Quick Start
### Recommended Usage
## The two pipelines
### meetus — meetings
```bash
python process_meeting.py samples/meeting.mkv --embed-images --scene-detection --scene-threshold 10 --diarize
uv run process_meeting.py samples/meeting.mkv --embed-images --scene-detection --scene-threshold 10 --diarize
```
Runs WhisperX (speaker diarization) + FFmpeg scene-detection frames → an enhanced
transcript with frame references, in `output/<YYYYMMDD-NNN-stem>/`. `--out-format`
customizes the run-folder name (e.g. `{name}.meetus` for a docs-coherent sidecar).
Batch a tree with `make batch IN=<dir>`. `process_meeting.py --help` is the flag
source of truth; caching (`--no-cache`, `--skip-cache-frames/-whisper`) lets you
iterate on scene thresholds without re-running Whisper.
This will:
1. Run WhisperX transcription with speaker diarization
2. Extract frames at scene changes (threshold 10 = moderately sensitive)
3. Create an enhanced transcript with frame file references
4. Save everything to `output/` folder
The `--embed-images` flag adds frame paths to the transcript (e.g., `Frame: frames/video_00257.jpg`), keeping the transcript small while frames stay in `frames/` folder for LLM access.
### Re-run with Cached Results
Already ran it once? Re-run instantly using cached results:
### doocus — documents
```bash
# Uses cached transcript and frames
python process_meeting.py samples/meeting.mkv --embed-images
# Skip only specific cached items
python process_meeting.py samples/meeting.mkv --embed-images --skip-cache-frames
python process_meeting.py samples/meeting.mkv --embed-images --skip-cache-whisper
# Force complete reprocessing
python process_meeting.py samples/meeting.mkv --embed-images --scene-detection --diarize --no-cache
uv run process_tree.py "/path/to/drive" --output "doocus-data/<source>" --only docs
```
Replicates the whole tree into `index.json` (every file a node), extracting text
sidecars (`content.md` + `meta.json`) only for `docx/pdf/pptx/xlsx`; other files
are linked, not duplicated. `--only {all|docs|meetings}` scopes a collection.
Full manual: [`doocus/README.md`](doocus/README.md).
## Usage Examples
## Outputs & collections
### Scene Detection Options
Each app writes to a **managed root** (gitignored), one subfolder per source,
mirroring the source structure:
- `doocus-data/<source>/` — document collections (`index.json` + `.doocus/`)
- `meetus-data/<source>/` — meeting collections (`index.json` + `.meetus/`)
Collections whose `index.json.root` matches are the **same source**: the doocus UI
merges their docs + meetings into one tree, toggled as one source. A meeting's
`.meetus` must sit in the **same folder** as its collection's `index.json`.
## The UIs
Local Vue 3 + Vite apps over `ui/framework/` (shared components + design tokens):
- **`ui/meetus-app`** — review a meeting: video + transcript (edit/read, speaker
merge, select mode) + frame selector, time-synced.
- **`ui/doocus-app`** — browse the merged tree, **search** cached text (transcripts,
extracted docs, raw files), per-type viewers, and **package** selected files.
Meetings **embed the meetus review** (`@review`, composed — not duplicated).
```bash
# Default threshold (15)
python process_meeting.py samples/meeting.mkv --embed-images --scene-detection --diarize
# More sensitive (more frames, threshold: 5)
python process_meeting.py samples/meeting.mkv --embed-images --scene-detection --scene-threshold 5 --diarize
# Less sensitive (fewer frames, threshold: 30)
python process_meeting.py samples/meeting.mkv --embed-images --scene-detection --scene-threshold 30 --diarize
cd ui/doocus-app && DOOCUS_DATA="$PWD/../../doocus-data,$PWD/../../meetus-data" npm run dev
cd ui/meetus-app && MEETUS_OUTPUT=/abs/path/to/output npm run dev
```
### Fixed Interval Extraction (alternative to scene detection)
```bash
# Every 10 seconds
python process_meeting.py samples/meeting.mkv --embed-images --interval 10 --diarize
## Packaging (the point)
# Every 3 seconds (more detailed)
python process_meeting.py samples/meeting.mkv --embed-images --interval 3 --diarize
The doocus package builder zips selected **originals** + their `content.md` for a
target (Gemini web ≤10 files/≤100 MB, or NotebookLM). Originals are what the
permitted services consume; the extracted text is the local **search index** and is
never a replacement for the document.
## Project layout
See [`INDEX.md`](INDEX.md) for the full map. In brief:
```
process_meeting.py meetus CLI process_tree.py / process_doc.py doocus CLIs
meetus/ meetus core doocus/ doocus core
ctrl/ batch + ops scripts ui/{meetus-app,doocus-app,framework}
Makefile batch · docs · merge · graphs docs/graphs/ architecture diagram
def/ design notes samples/ output/ (gitignored)
```
### Caching Examples
```bash
# First run - processes everything
python process_meeting.py samples/meeting.mkv --embed-images --scene-detection --scene-threshold 10 --diarize
# Iterate on scene threshold (reuse whisper transcript)
python process_meeting.py samples/meeting.mkv --embed-images --scene-detection --scene-threshold 5 --skip-cache-frames
# Re-run whisper only
python process_meeting.py samples/meeting.mkv --embed-images --skip-cache-whisper
# Force complete reprocessing
python process_meeting.py samples/meeting.mkv --embed-images --scene-detection --diarize --no-cache
```
### Custom output location
```bash
python process_meeting.py samples/meeting.mkv --embed-images --scene-detection --diarize --output-dir my_outputs/
```
### Enable verbose logging
```bash
python process_meeting.py samples/meeting.mkv --embed-images --scene-detection --diarize --verbose
```
## Output Files
Each video gets its own timestamped output directory:
```
output/
└── 20241019_143022-meeting/
├── manifest.json # Processing configuration
├── meeting_enhanced.txt # Enhanced transcript for AI
├── meeting.json # Whisper/WhisperX transcript
└── frames/ # Extracted video frames
├── frame_00001_5.00s.jpg
├── frame_00002_10.00s.jpg
└── ...
```
### Caching Behavior
The tool automatically reuses the most recent output directory for the same video:
- **First run**: Creates new timestamped directory (e.g., `20241019_143022-meeting/`)
- **Subsequent runs**: Reuses the same directory and cached results
- **Cached items**: Whisper transcript, extracted frames, analysis results
**Fine-grained cache control:**
- `--no-cache`: Force complete reprocessing
- `--skip-cache-frames`: Re-extract frames only
- `--skip-cache-whisper`: Re-run transcription only
This allows you to iterate on scene detection thresholds without re-running Whisper!
## Workflow for Meeting Analysis
### Complete Workflow (One Command!)
```bash
python process_meeting.py samples/meeting.mkv --embed-images --scene-detection --scene-threshold 10 --diarize
```
### Typical Iterative Workflow
```bash
# First run - full processing
python process_meeting.py samples/meeting.mkv --embed-images --scene-detection --scene-threshold 10 --diarize
# Adjust scene threshold (keeps cached whisper transcript)
python process_meeting.py samples/meeting.mkv --embed-images --scene-detection --scene-threshold 5 --skip-cache-frames
```
### Example Prompt for Claude
```
Please summarize this meeting transcript. Pay special attention to:
1. Key decisions made
2. Action items
3. Technical details shown on screen
4. Any metrics or data presented
[Paste enhanced transcript here]
```
## Command Reference
`process_meeting.py --help` is the source of truth for flags — run it rather than
relying on a copy here. The essentials:
- `--diarize` — WhisperX with speaker diarization (needs a HuggingFace token)
- `--embed-images` — reference frames in the transcript for the LLM (default behavior)
- `--scene-detection` / `--scene-threshold N` — frame extraction (lower = more frames)
- `--interval N` — fixed-interval extraction instead of scene detection
- `--transcript-formats srt,vtt,…` — extra transcript formats alongside JSON
- `--no-cache` / `--skip-cache-frames` / `--skip-cache-whisper` — cache control
For batches, prefer `make batch IN=<dir>` (see [`INDEX.md`](INDEX.md)).
## Tips for Best Results
### Scene Detection vs Interval
- **Scene detection** (`--scene-detection`): Recommended. Captures frames when content changes. More efficient.
- **Interval extraction** (`--interval N`): Alternative for continuous content. Captures every N seconds.
### Scene Detection Threshold
- Lower values (5-10): More sensitive, captures more frames
- Default (15): Good balance for most meetings
- Higher values (20-30): Less sensitive, fewer frames
### Whisper vs WhisperX
- **Whisper** (`--run-whisper`): Standard transcription, fast
- **WhisperX** (`--run-whisper --diarize`): Adds speaker identification, requires HuggingFace token
## Troubleshooting
### Frame Extraction Issues
**"No frames extracted"**
- Check video file is valid: `ffmpeg -i video.mkv`
- Try lower scene threshold: `--scene-threshold 5`
- Try interval extraction: `--interval 3`
- Check disk space in output directory
**Scene detection not working**
- Ensure FFmpeg is installed
- Falls back to interval extraction automatically
- Try manual interval: `--interval 5`
### Whisper/WhisperX Issues
**WhisperX diarization not working**
- Ensure you have a HuggingFace token set
- Token needs access to pyannote models
- Fall back to standard Whisper without `--diarize`
### Cache Issues
**Cache not being used**
- Ensure you're using the same video filename
- Check that output directory contains cached files
- Use `--verbose` to see what's being cached/loaded
**Want to re-run specific steps**
- `--skip-cache-frames`: Re-extract frames
- `--skip-cache-whisper`: Re-run transcription
- `--no-cache`: Force complete reprocessing
## Deprecated Features (kept for reference)
### OCR and Vision Analysis
OCR, Vision and Hybrid screen-text analysis were the original approach but went nowhere. They have been **removed from the CLI** (the `--ocr-engine` / `--use-vision` / `--use-hybrid` flags no longer exist) and now live, unwired, in `meetus/deprecated/` for reference only. The tool always references frames (`--embed-images`) so your LLM reads them directly. The realtime continuation of the idea is the separate `cht` project. See [`INDEX.md`](INDEX.md).
## Project Structure
See [`INDEX.md`](INDEX.md) for the full repo map. In brief:
```
meetus/
├── process_meeting.py # Main CLI script (entry point)
├── Makefile # `make batch` convenience wrapper
├── meetus/ # Core package
│ ├── workflow.py # Processing orchestrator
│ ├── frame_extractor.py # Frame extraction (FFmpeg scene detection)
│ ├── transcript_merger.py # Transcript + frame-ref merging
│ ├── output_manager.py # Run dirs (YYYYMMDD-NNN-<stem>) & manifest
│ ├── cache_manager.py # Per-step caching
│ └── deprecated/ # Old OCR/vision/hybrid analysis (reference only)
├── ctrl/ # Control plane / operational scripts
│ ├── batch.sh # Recursive batch runner
│ ├── transcribe_oneoff.sh # High-quality re-transcription
│ ├── summarize/ # Local-LLM summarization (WIP, on hold)
│ └── cht/ # Bridge to the realtime `cht` project
├── def/ # Design/decision notes
├── output/ # Run directories (gitignored)
├── samples/ # Sample inputs (gitignored)
└── README.md # This file
```
Deprecated OCR/vision code lives unwired in `meetus/deprecated/` (the only user of
`ollama`, kept out of every default install). See [`INDEX.md`](INDEX.md).
## License
For personal use.

View File

@@ -7,14 +7,19 @@
# the mirrored output subfolder.
#
# Usage:
# ctrl/batch.sh -i <input-dir> -o <output-dir> [-e "mkv mp4 ..."] [-n] \
# [-- <process_meeting.py flags>]
# ctrl/batch.sh -i <input-dir> -o <output-dir> [-e "mkv mp4 ..."] \
# [-l <list-file>] [-n] [-- <process_meeting.py flags>]
#
# Examples:
# # Everything under a mounted share, default extraction flags forwarded
# ctrl/batch.sh -i "/mnt/win/trainings" -o output/batch \
# -- --embed-images --scene-detection --diarize
#
# # Only the meetings doocus found (relative paths), re-rooted at a new mount,
# # output into the doocus folder for later packaging:
# ctrl/batch.sh -i "/mnt/drive" -o docs-output -l docs-output/meetings.txt \
# -- --embed-images --scene-detection --diarize
#
# # Dry run: just show which videos map to which output folders
# ctrl/batch.sh -i "/mnt/win/trainings" -o output/batch -n
#
@@ -42,6 +47,7 @@ INPUT=""
OUTPUT=""
EXTS="mkv mp4 mov avi m4v webm wmv ogg mp3 wav m4a opus flac aac"
DRY=false
LIST="" # optional newline list of RELATIVE paths (re-rooted at INPUT)
FORWARD=()
while [[ $# -gt 0 ]]; do
@@ -49,6 +55,7 @@ while [[ $# -gt 0 ]]; do
-i|--input) INPUT="$2"; shift 2 ;;
-o|--output) OUTPUT="$2"; shift 2 ;;
-e|--ext) EXTS="$2"; shift 2 ;;
-l|--list) LIST="$2"; shift 2 ;;
-n|--dry-run) DRY=true; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; FORWARD=("$@"); break ;;
@@ -59,6 +66,7 @@ done
[ -n "$INPUT" ] || { echo "ERROR: -i/--input is required" >&2; exit 1; }
[ -n "$OUTPUT" ] || { echo "ERROR: -o/--output is required" >&2; exit 1; }
[ -d "$INPUT" ] || { echo "ERROR: input dir not found: $INPUT" >&2; exit 1; }
[ -z "$LIST" ] || [ -f "$LIST" ] || { echo "ERROR: list file not found: $LIST" >&2; exit 1; }
# Absolute paths so relative-path math is stable regardless of where we cd'd.
INPUT="$(realpath "$INPUT")"
@@ -74,16 +82,35 @@ unset 'find_expr[${#find_expr[@]}-1]' # drop the trailing -o
echo "Input : $INPUT"
echo "Output: $OUTPUT"
echo "Exts : $EXTS"
[ -n "$LIST" ] && echo "List : $LIST (relative paths, re-rooted at input)" || echo "Exts : $EXTS"
[ "$DRY" = true ] && echo "(dry run — nothing will be processed)"
echo
# Producer: either the given list of relative paths (re-rooted at INPUT) or a
# recursive find by extension. Both emit null-delimited absolute paths.
producer() {
if [ -n "$LIST" ]; then
while IFS= read -r rel || [ -n "$rel" ]; do
[ -z "$rel" ] && continue
printf '%s\0' "$INPUT/$rel"
done < "$LIST"
else
find "$INPUT" -type f \( "${find_expr[@]}" \) -print0 | sort -z
fi
}
total=0 ok=0 fail=0
# Process substitution (not a pipe) so counters survive into the summary.
while IFS= read -r -d '' video; do
total=$((total + 1))
rel="${video#"$INPUT"/}" # path relative to the input root
if [ ! -f "$video" ]; then # list may name a path missing under this root
echo "[$total] $rel !! not found under input (skipped)" >&2
fail=$((fail + 1))
continue
fi
reldir="$(dirname "$rel")"
if [ "$reldir" = "." ]; then
outdir="$OUTPUT" # video sat directly in the input root
@@ -110,7 +137,7 @@ while IFS= read -r -d '' video; do
fail=$((fail + 1))
fi
echo
done < <(find "$INPUT" -type f \( "${find_expr[@]}" \) -print0 | sort -z)
done < <(producer)
echo "----------------------------------------"
if [ "$DRY" = true ]; then

View File

@@ -0,0 +1,53 @@
// Architecture of the meetus + doocus toolkit.
// Regenerate the SVG with: make graphs (or: dot -Tsvg docs/graphs/architecture.dot -o docs/graphs/architecture.svg)
digraph meetus_doocus {
rankdir=LR
splines=ortho
fontname="Helvetica"
node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=11, fillcolor="#f5f5f5", color="#cccccc"]
edge [fontname="Helvetica", fontsize=9, color="#888888"]
drive [label="Downloaded Drive\n(local · sensitive)", fillcolor="#e8eef7"]
subgraph cluster_pipe {
label="Deterministic, offline pipelines — no cloud AI touches the source"
labeljust=l ; style=dashed ; color="#bbbbbb" ; fontsize=10
meetus_cli [label="meetus\nprocess_meeting.py · make batch\nwhisperx + ffmpeg frames"]
doocus_cli [label="doocus\nprocess_tree.py · process_doc.py\nlocal text extraction"]
}
subgraph cluster_out {
label="Managed collection roots (gitignored) — <root>/<source>/, mirrored"
labeljust=l ; style=dashed ; color="#bbbbbb" ; fontsize=10
meetus_data [label="meetus-data/<source>/\nindex.json + <file>.meetus/\ntranscript · frames", fillcolor="#fdf0e3"]
doocus_data [label="doocus-data/<source>/\nindex.json + <file>.doocus/\ncontent.md · meta.json", fillcolor="#eaf6ea"]
}
subgraph cluster_ui {
label="Local UIs (Vue 3 + Vite)"
labeljust=l ; style=dashed ; color="#bbbbbb" ; fontsize=10
doocus_app [label="doocus-app\ntree · search · package\nembeds the meeting review"]
meetus_app [label="meetus-app\nmeeting review\nvideo · transcript · frames"]
review [label="@review\n(meetus ReviewBody — composed by both)", fillcolor="#f3ecfa"]
framework [label="ui/framework\nshared components + tokens", fillcolor="#f3ecfa"]
}
package [label="Package (zip)\noriginals + content.md", fillcolor="#e8eef7"]
services [label="Gemini web · NotebookLM\n(permitted services)", shape=ellipse, fillcolor="#e8eef7"]
drive -> meetus_cli
drive -> doocus_cli
meetus_cli -> meetus_data
doocus_cli -> doocus_data
doocus_data -> doocus_app
meetus_data -> doocus_app [xlabel="merge by\nshared root"]
meetus_data -> meetus_app
framework -> review [style=dashed, arrowhead=none]
review -> meetus_app [style=dashed, xlabel="standalone"]
review -> doocus_app [style=dashed, xlabel="embedded"]
doocus_app -> package
package -> services
}

View File

@@ -0,0 +1,186 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.42.4 (0)
-->
<!-- Title: meetus_doocus Pages: 1 -->
<svg width="1517pt" height="314pt"
viewBox="0.00 0.00 1516.69 314.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 310)">
<title>meetus_doocus</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-310 1512.69,-310 1512.69,4 -4,4"/>
<g id="clust1" class="cluster">
<title>cluster_pipe</title>
<polygon fill="none" stroke="#bbbbbb" stroke-dasharray="5,2" points="145,-157 145,-298 477,-298 477,-157 145,-157"/>
<text text-anchor="middle" x="311" y="-286" font-family="Helvetica,sans-Serif" font-size="10.00">Deterministic, offline pipelines — no cloud AI touches the source</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_out</title>
<polygon fill="none" stroke="#bbbbbb" stroke-dasharray="5,2" points="505,-157 505,-298 859,-298 859,-157 505,-157"/>
<text text-anchor="middle" x="682" y="-286" font-family="Helvetica,sans-Serif" font-size="10.00">Managed collection roots (gitignored) — &lt;root&gt;/&lt;source&gt;/, mirrored</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_ui</title>
<polygon fill="none" stroke="#bbbbbb" stroke-dasharray="5,2" points="213,-8 213,-149 1074,-149 1074,-8 213,-8"/>
<text text-anchor="middle" x="279" y="-137" font-family="Helvetica,sans-Serif" font-size="10.00">Local UIs (Vue 3 + Vite)</text>
</g>
<!-- drive -->
<g id="node1" class="node">
<title>drive</title>
<path fill="#e8eef7" stroke="#cccccc" d="M105,-236C105,-236 12,-236 12,-236 6,-236 0,-230 0,-224 0,-224 0,-212 0,-212 0,-206 6,-200 12,-200 12,-200 105,-200 105,-200 111,-200 117,-206 117,-212 117,-212 117,-224 117,-224 117,-230 111,-236 105,-236"/>
<text text-anchor="middle" x="58.5" y="-221.2" font-family="Helvetica,sans-Serif" font-size="11.00">Downloaded Drive</text>
<text text-anchor="middle" x="58.5" y="-209.2" font-family="Helvetica,sans-Serif" font-size="11.00">(local · sensitive)</text>
</g>
<!-- meetus_cli -->
<g id="node2" class="node">
<title>meetus_cli</title>
<path fill="#f5f5f5" stroke="#cccccc" d="M400,-209C400,-209 221,-209 221,-209 215,-209 209,-203 209,-197 209,-197 209,-177 209,-177 209,-171 215,-165 221,-165 221,-165 400,-165 400,-165 406,-165 412,-171 412,-177 412,-177 412,-197 412,-197 412,-203 406,-209 400,-209"/>
<text text-anchor="middle" x="310.5" y="-196.2" font-family="Helvetica,sans-Serif" font-size="11.00">meetus</text>
<text text-anchor="middle" x="310.5" y="-184.2" font-family="Helvetica,sans-Serif" font-size="11.00">process_meeting.py · make batch</text>
<text text-anchor="middle" x="310.5" y="-172.2" font-family="Helvetica,sans-Serif" font-size="11.00">whisperx + ffmpeg frames</text>
</g>
<!-- drive&#45;&gt;meetus_cli -->
<g id="edge1" class="edge">
<title>drive&#45;&gt;meetus_cli</title>
<path fill="none" stroke="#888888" d="M117.26,-204.5C117.26,-204.5 198.64,-204.5 198.64,-204.5"/>
<polygon fill="#888888" stroke="#888888" points="198.64,-208 208.64,-204.5 198.64,-201 198.64,-208"/>
</g>
<!-- doocus_cli -->
<g id="node3" class="node">
<title>doocus_cli</title>
<path fill="#f5f5f5" stroke="#cccccc" d="M398.5,-271C398.5,-271 222.5,-271 222.5,-271 216.5,-271 210.5,-265 210.5,-259 210.5,-259 210.5,-239 210.5,-239 210.5,-233 216.5,-227 222.5,-227 222.5,-227 398.5,-227 398.5,-227 404.5,-227 410.5,-233 410.5,-239 410.5,-239 410.5,-259 410.5,-259 410.5,-265 404.5,-271 398.5,-271"/>
<text text-anchor="middle" x="310.5" y="-258.2" font-family="Helvetica,sans-Serif" font-size="11.00">doocus</text>
<text text-anchor="middle" x="310.5" y="-246.2" font-family="Helvetica,sans-Serif" font-size="11.00">process_tree.py · process_doc.py</text>
<text text-anchor="middle" x="310.5" y="-234.2" font-family="Helvetica,sans-Serif" font-size="11.00">local text extraction</text>
</g>
<!-- drive&#45;&gt;doocus_cli -->
<g id="edge2" class="edge">
<title>drive&#45;&gt;doocus_cli</title>
<path fill="none" stroke="#888888" d="M117.26,-231.5C117.26,-231.5 200.13,-231.5 200.13,-231.5"/>
<polygon fill="#888888" stroke="#888888" points="200.14,-235 210.13,-231.5 200.13,-228 200.14,-235"/>
</g>
<!-- meetus_data -->
<g id="node4" class="node">
<title>meetus_data</title>
<path fill="#fdf0e3" stroke="#cccccc" d="M756,-209C756,-209 607,-209 607,-209 601,-209 595,-203 595,-197 595,-197 595,-177 595,-177 595,-171 601,-165 607,-165 607,-165 756,-165 756,-165 762,-165 768,-171 768,-177 768,-177 768,-197 768,-197 768,-203 762,-209 756,-209"/>
<text text-anchor="middle" x="681.5" y="-196.2" font-family="Helvetica,sans-Serif" font-size="11.00">meetus&#45;data/&lt;source&gt;/</text>
<text text-anchor="middle" x="681.5" y="-184.2" font-family="Helvetica,sans-Serif" font-size="11.00">index.json + &lt;file&gt;.meetus/</text>
<text text-anchor="middle" x="681.5" y="-172.2" font-family="Helvetica,sans-Serif" font-size="11.00">transcript · frames</text>
</g>
<!-- meetus_cli&#45;&gt;meetus_data -->
<g id="edge3" class="edge">
<title>meetus_cli&#45;&gt;meetus_data</title>
<path fill="none" stroke="#888888" d="M412.37,-187C412.37,-187 584.95,-187 584.95,-187"/>
<polygon fill="#888888" stroke="#888888" points="584.95,-190.5 594.95,-187 584.95,-183.5 584.95,-190.5"/>
</g>
<!-- doocus_data -->
<g id="node5" class="node">
<title>doocus_data</title>
<path fill="#eaf6ea" stroke="#cccccc" d="M754.5,-271C754.5,-271 608.5,-271 608.5,-271 602.5,-271 596.5,-265 596.5,-259 596.5,-259 596.5,-239 596.5,-239 596.5,-233 602.5,-227 608.5,-227 608.5,-227 754.5,-227 754.5,-227 760.5,-227 766.5,-233 766.5,-239 766.5,-239 766.5,-259 766.5,-259 766.5,-265 760.5,-271 754.5,-271"/>
<text text-anchor="middle" x="681.5" y="-258.2" font-family="Helvetica,sans-Serif" font-size="11.00">doocus&#45;data/&lt;source&gt;/</text>
<text text-anchor="middle" x="681.5" y="-246.2" font-family="Helvetica,sans-Serif" font-size="11.00">index.json + &lt;file&gt;.doocus/</text>
<text text-anchor="middle" x="681.5" y="-234.2" font-family="Helvetica,sans-Serif" font-size="11.00">content.md · meta.json</text>
</g>
<!-- doocus_cli&#45;&gt;doocus_data -->
<g id="edge4" class="edge">
<title>doocus_cli&#45;&gt;doocus_data</title>
<path fill="none" stroke="#888888" d="M410.9,-249C410.9,-249 586.43,-249 586.43,-249"/>
<polygon fill="#888888" stroke="#888888" points="586.43,-252.5 596.43,-249 586.43,-245.5 586.43,-252.5"/>
</g>
<!-- doocus_app -->
<g id="node6" class="node">
<title>doocus_app</title>
<path fill="#f5f5f5" stroke="#cccccc" d="M1054,-122C1054,-122 907,-122 907,-122 901,-122 895,-116 895,-110 895,-110 895,-90 895,-90 895,-84 901,-78 907,-78 907,-78 1054,-78 1054,-78 1060,-78 1066,-84 1066,-90 1066,-90 1066,-110 1066,-110 1066,-116 1060,-122 1054,-122"/>
<text text-anchor="middle" x="980.5" y="-109.2" font-family="Helvetica,sans-Serif" font-size="11.00">doocus&#45;app</text>
<text text-anchor="middle" x="980.5" y="-97.2" font-family="Helvetica,sans-Serif" font-size="11.00">tree · search · package</text>
<text text-anchor="middle" x="980.5" y="-85.2" font-family="Helvetica,sans-Serif" font-size="11.00">embeds the meeting review</text>
</g>
<!-- meetus_data&#45;&gt;doocus_app -->
<g id="edge6" class="edge">
<title>meetus_data&#45;&gt;doocus_app</title>
<path fill="none" stroke="#888888" d="M681.69,-164.89C681.69,-140.45 681.69,-104.5 681.69,-104.5 681.69,-104.5 884.87,-104.5 884.87,-104.5"/>
<polygon fill="#888888" stroke="#888888" points="884.87,-108 894.87,-104.5 884.87,-101 884.87,-108"/>
<text text-anchor="middle" x="727.08" y="-117.3" font-family="Helvetica,sans-Serif" font-size="9.00">merge by</text>
<text text-anchor="middle" x="727.08" y="-107.3" font-family="Helvetica,sans-Serif" font-size="9.00">shared root</text>
</g>
<!-- meetus_app -->
<g id="node7" class="node">
<title>meetus_app</title>
<path fill="#f5f5f5" stroke="#cccccc" d="M1049,-60C1049,-60 912,-60 912,-60 906,-60 900,-54 900,-48 900,-48 900,-28 900,-28 900,-22 906,-16 912,-16 912,-16 1049,-16 1049,-16 1055,-16 1061,-22 1061,-28 1061,-28 1061,-48 1061,-48 1061,-54 1055,-60 1049,-60"/>
<text text-anchor="middle" x="980.5" y="-47.2" font-family="Helvetica,sans-Serif" font-size="11.00">meetus&#45;app</text>
<text text-anchor="middle" x="980.5" y="-35.2" font-family="Helvetica,sans-Serif" font-size="11.00">meeting review</text>
<text text-anchor="middle" x="980.5" y="-23.2" font-family="Helvetica,sans-Serif" font-size="11.00">video · transcript · frames</text>
</g>
<!-- meetus_data&#45;&gt;meetus_app -->
<g id="edge7" class="edge">
<title>meetus_data&#45;&gt;meetus_app</title>
<path fill="none" stroke="#888888" d="M768.1,-187C810.98,-187 853.69,-187 853.69,-187 853.69,-187 853.69,-57 853.69,-57 853.69,-57 889.83,-57 889.83,-57"/>
<polygon fill="#888888" stroke="#888888" points="889.83,-60.5 899.83,-57 889.83,-53.5 889.83,-60.5"/>
</g>
<!-- doocus_data&#45;&gt;doocus_app -->
<g id="edge5" class="edge">
<title>doocus_data&#45;&gt;doocus_app</title>
<path fill="none" stroke="#888888" d="M766.83,-249C855.27,-249 980.69,-249 980.69,-249 980.69,-249 980.69,-132.31 980.69,-132.31"/>
<polygon fill="#888888" stroke="#888888" points="984.19,-132.31 980.69,-122.31 977.19,-132.31 984.19,-132.31"/>
</g>
<!-- package -->
<g id="node10" class="node">
<title>package</title>
<path fill="#e8eef7" stroke="#cccccc" d="M1233,-118C1233,-118 1114,-118 1114,-118 1108,-118 1102,-112 1102,-106 1102,-106 1102,-94 1102,-94 1102,-88 1108,-82 1114,-82 1114,-82 1233,-82 1233,-82 1239,-82 1245,-88 1245,-94 1245,-94 1245,-106 1245,-106 1245,-112 1239,-118 1233,-118"/>
<text text-anchor="middle" x="1173.5" y="-103.2" font-family="Helvetica,sans-Serif" font-size="11.00">Package (zip)</text>
<text text-anchor="middle" x="1173.5" y="-91.2" font-family="Helvetica,sans-Serif" font-size="11.00">originals + content.md</text>
</g>
<!-- doocus_app&#45;&gt;package -->
<g id="edge11" class="edge">
<title>doocus_app&#45;&gt;package</title>
<path fill="none" stroke="#888888" d="M1066.28,-100C1066.28,-100 1091.84,-100 1091.84,-100"/>
<polygon fill="#888888" stroke="#888888" points="1091.84,-103.5 1101.84,-100 1091.84,-96.5 1091.84,-103.5"/>
</g>
<!-- review -->
<g id="node8" class="node">
<title>review</title>
<path fill="#f3ecfa" stroke="#cccccc" d="M799,-87C799,-87 564,-87 564,-87 558,-87 552,-81 552,-75 552,-75 552,-63 552,-63 552,-57 558,-51 564,-51 564,-51 799,-51 799,-51 805,-51 811,-57 811,-63 811,-63 811,-75 811,-75 811,-81 805,-87 799,-87"/>
<text text-anchor="middle" x="681.5" y="-72.2" font-family="Helvetica,sans-Serif" font-size="11.00">@review</text>
<text text-anchor="middle" x="681.5" y="-60.2" font-family="Helvetica,sans-Serif" font-size="11.00">(meetus ReviewBody — composed by both)</text>
</g>
<!-- review&#45;&gt;doocus_app -->
<g id="edge10" class="edge">
<title>review&#45;&gt;doocus_app</title>
<path fill="none" stroke="#888888" stroke-dasharray="5,2" d="M811.34,-82.5C811.34,-82.5 884.71,-82.5 884.71,-82.5"/>
<polygon fill="#888888" stroke="#888888" points="884.71,-86 894.71,-82.5 884.71,-79 884.71,-86"/>
<text text-anchor="middle" x="872.53" y="-75.3" font-family="Helvetica,sans-Serif" font-size="9.00">embedded</text>
</g>
<!-- review&#45;&gt;meetus_app -->
<g id="edge9" class="edge">
<title>review&#45;&gt;meetus_app</title>
<path fill="none" stroke="#888888" stroke-dasharray="5,2" d="M811.34,-54C811.34,-54 889.9,-54 889.9,-54"/>
<polygon fill="#888888" stroke="#888888" points="889.9,-57.5 899.9,-54 889.9,-50.5 889.9,-57.5"/>
<text text-anchor="middle" x="875.62" y="-46.8" font-family="Helvetica,sans-Serif" font-size="9.00">standalone</text>
</g>
<!-- framework -->
<g id="node9" class="node">
<title>framework</title>
<path fill="#f3ecfa" stroke="#cccccc" d="M388,-87C388,-87 233,-87 233,-87 227,-87 221,-81 221,-75 221,-75 221,-63 221,-63 221,-57 227,-51 233,-51 233,-51 388,-51 388,-51 394,-51 400,-57 400,-63 400,-63 400,-75 400,-75 400,-81 394,-87 388,-87"/>
<text text-anchor="middle" x="310.5" y="-72.2" font-family="Helvetica,sans-Serif" font-size="11.00">ui/framework</text>
<text text-anchor="middle" x="310.5" y="-60.2" font-family="Helvetica,sans-Serif" font-size="11.00">shared components + tokens</text>
</g>
<!-- framework&#45;&gt;review -->
<g id="edge8" class="edge">
<title>framework&#45;&gt;review</title>
<path fill="none" stroke="#888888" stroke-dasharray="5,2" d="M400.31,-69C445.77,-69 501.8,-69 551.84,-69"/>
</g>
<!-- services -->
<g id="node11" class="node">
<title>services</title>
<ellipse fill="#e8eef7" stroke="#cccccc" cx="1394.84" cy="-100" rx="113.69" ry="22.76"/>
<text text-anchor="middle" x="1394.84" y="-103.2" font-family="Helvetica,sans-Serif" font-size="11.00">Gemini web · NotebookLM</text>
<text text-anchor="middle" x="1394.84" y="-91.2" font-family="Helvetica,sans-Serif" font-size="11.00">(permitted services)</text>
</g>
<!-- package&#45;&gt;services -->
<g id="edge12" class="edge">
<title>package&#45;&gt;services</title>
<path fill="none" stroke="#888888" d="M1245.06,-100C1245.06,-100 1270.89,-100 1270.89,-100"/>
<polygon fill="#888888" stroke="#888888" points="1270.89,-103.5 1280.89,-100 1270.89,-96.5 1270.89,-103.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,37 +1,74 @@
# doocus — local document extraction
# doocus — local drive tree indexer
The document twin of meetus. Turns locally-downloaded files (Drive exports,
PDFs, images, ...) into a shareable **textified** product (`content.md`) plus
structured `meta.json`, mirroring the meetus `output/<run>/` contract.
The document twin of meetus. Replicates a **whole local drive tree** into a
single `index.json`, preserving the folder hierarchy (no flattening). Every file
is a node; the **original file is the artifact** (what a QA/PM opens, what a
package links to). doocus only *extracts text* for the heavy formats
(docx/pdf/pptx/xlsx) — and that text is a **search index only, never a
replacement** for the document.
Extraction is **deterministic and offline** — no cloud AI touches the raw source.
The textified output is what feeds permitted services (Gemini web, NotebookLM)
and the local search index; the raw source stays on disk.
The raw source stays on disk; permitted services (Gemini web, NotebookLM) get the
original (as a link once we have Drive URLs, or re-uploaded).
## Quick start
```bash
# one document
python process_doc.py notes.docx # → docs-output/<YYYYMMDD-NNN-notes>/
python process_doc.py report.pdf --render # also render page 1 → thumb.jpg
python process_doc.py scan.png --ocr # OCR the image into content.md
# index a whole downloaded drive tree → docs-output/index.json
uv run make docs IN="/mnt/win/drive"
uv run make docs IN="..." DOC_EXTRA="--ocr" # OCR scanned pdfs
make docs-dry IN="..." # counts only, no writes
# a whole downloaded folder (mirrors the input tree)
make docs IN="/mnt/win/drive" # → docs-output/...
make docs IN="..." DOC_EXTRA="--render --ocr"
make docs-dry IN="..." # list what would run
# or directly
uv run process_tree.py /mnt/win/drive --output docs-output
# one-off single file (older per-file model, still handy)
uv run process_doc.py notes.docx
```
## Output contract
```
docs-output/<YYYYMMDD-NNN-stem>/
content.md textified document — the shareable product
meta.json source info, sha256, dates, author, title, pages/sheets/slides, word_count, warnings
thumb.jpg optional single preview (image / rendered page / video frame)
manifest.json run config + outputs inventory + source pointer (path, not a copy)
docs-output/
index.json whole tree; every file a node:
{ path, name, ext, family, mode, bytes,
modified, url:null } ◀ url = Drive link (later)
<mirrored path>/<file>.doocus/ sidecar — ONLY for extracted formats:
├── content.md extracted text — SEARCH INDEX ONLY
└── meta.json source/sha/dates/author/pages/... metadata
```
Node **modes**: `extracted` (docx/pdf/pptx/xlsx → text sidecar) · `meeting`
(mp4/mkv/... → belongs to meetus, not a doc) · `link` (everything else → original
is the artifact, nothing duplicated). Every node also carries `created` (birth
time, falling back to mtime) so docs and meetings sort by creation date coherently.
## Meetings
Videos are `meeting` nodes — doocus does **not** transcribe them; meetus does. To
keep everything in one coherent, searchable tree, run meetus over **only** the
meetings doocus found and write each into a `<file>.meetus/` sidecar right next to
its `<file>.doocus/`-style neighbours:
```bash
# 1. index the drive → docs-output/{index.json, meetings.txt}
uv run make docs IN="/mnt/drive"
# 2. meetus over ONLY the listed meetings, re-rooted at the mount, into
# <file>.meetus/ sidecars (matches the pointer doocus already wrote):
make batch IN="/mnt/drive" OUT=docs-output \
LIST=docs-output/meetings.txt \
OUT_FORMAT="{name}.meetus"
```
- `meetings.txt` holds **relative** paths only — mount the drive anywhere and the
`-l/--list` re-roots them under `-i`, so this machine reads only the meetings.
- `OUT_FORMAT="{name}.meetus"` is meetus's new `--out-format` (tokens `{date}
{run} {stem} {name}`; default keeps `YYYYMMDD-NNN-<stem>`). Omitting `{run}`
makes the folder deterministic, so it matches the `out`/`transcript` pointer the
indexer set on each `meeting` node — no relink step.
- Frames land in `<file>.meetus/frames/` for now (unchanged).
## Supported types
| Family | Extensions | Notes |
@@ -63,14 +100,38 @@ uv sync --group doocus --group ocr # + pytesseract (needs system `tesseract`
(`--render`) additionally needs the `pdf-render` group (`pdf2image`) + system
`poppler`.
## Collections (multiple sources)
doocus-app discovers collections from **managed roots** (gitignored),
`<root>/<source>/` per source:
- `doocus-data/<source>/` — document collections
- `meetus-data/<source>/` — meeting collections (`.meetus` + a `--only meetings` index)
Override the roots with `DOOCUS_DATA` (comma-separated; `DOOCUS_OUTPUT` still works
for a single dir). **Collections whose `index.json.root` match are the same source**
— the UI merges their docs + meetings into one interleaved tree, toggled together
from the `` sources menu. When the same file exists in two collections, the copy
whose sidecar resolves wins (a `meetus-data` meeting with its `.meetus` overrides a
bare `doocus` copy).
## Browser UI
`ui/doocus-app/` (sibling of `ui/meetus-app/`, shares `ui/framework/`) browses the
extracted docs with per-type viewers, a metadata panel, and a package builder
that zips the **original file + content.md + meta.json** per doc for a chosen
target (Gemini web / NotebookLM):
`ui/doocus-app/` (sibling of `ui/meetus-app/`, shares `ui/framework/`):
- **Tree** (folders preserved, source-grouped) with a **search bar** — server-side
search over the cached text (transcripts, extracted `content.md`, raw files);
matches **prune the tree** and show a file count + size.
- **Detail**: for extracted docs, markdown-rendered **extracted text** (labelled
"search index, not the document") beside the **native original** (pdf inline;
docx/xlsx rendered via mammoth/SheetJS); linked files render directly; **meetings
embed the meetus review** (video + transcript + frames, from `@review`).
- **Scan** a folder from the UI (`` menu → Scan) — runs `process_tree` via `uv`
into `doocus-data/`; **⟳** re-discovers newly-scanned sources.
- **Package** builder zips selected **originals** (+ `content.md` for extracted) for
a target (Gemini web / NotebookLM); Drive **links** once nodes carry a `url`.
```bash
cd ui/doocus-app && npm install
DOOCUS_OUTPUT=/path/to/docs-output npm run dev
DOOCUS_DATA="/abs/doocus-data,/abs/meetus-data" npm run dev
```

View File

@@ -11,9 +11,29 @@ from .base import ExtractionResult
MAX_SHEET_ROWS = 200
# Generic core-property titles Office writes by default — worse than useless as a
# display title (they hide the real one), so we drop them and derive from content.
PLACEHOLDER_TITLES = {
"word document", "powerpoint presentation", "excel workbook",
"microsoft word document", "microsoft powerpoint presentation",
"microsoft excel worksheet", "presentation", "workbook", "document",
}
def _good_title(t) -> bool:
return bool(t) and str(t).strip().lower() not in PLACEHOLDER_TITLES
def _first_line(text: str, limit: int = 120) -> str:
for ln in text.splitlines():
s = ln.lstrip("#").strip()
if s:
return s[:limit]
return ""
def _core_props(props) -> dict:
"""Common OOXML core properties → metadata (only non-empty ones).
"""Common OOXML core properties → metadata (only non-empty, non-placeholder).
python-docx/pptx expose `.author`/`.last_modified_by`; openpyxl uses
`.creator`/`.lastModifiedBy`. Each target checks both spellings.
@@ -31,6 +51,8 @@ def _core_props(props) -> dict:
for src in sources:
val = getattr(props, src, None)
if val:
if dst == "title" and not _good_title(val):
continue # skip "Word Document" & friends; derive from content
out[dst] = str(val)
break
return out
@@ -43,6 +65,10 @@ def _extract_docx(source: Path) -> ExtractionResult:
content = "\n\n".join(p for p in paras if p.strip()) + "\n"
metadata = _core_props(doc.core_properties)
metadata["paragraphs"] = len([p for p in paras if p.strip()])
if "title" not in metadata:
t = _first_line(content)
if t:
metadata["title"] = t
return ExtractionResult(content=content, metadata=metadata, extractor="office/python-docx@1")
@@ -62,6 +88,12 @@ def _extract_pptx(source: Path) -> ExtractionResult:
content = "\n\n".join(blocks) + "\n"
metadata = _core_props(prs.core_properties)
metadata["slides"] = len(prs.slides)
if "title" not in metadata and prs.slides:
# first slide's title placeholder, if any
for shape in prs.slides[0].shapes:
if shape.has_text_frame and shape.text_frame.text.strip():
metadata["title"] = shape.text_frame.text.strip()[:120]
break
return ExtractionResult(content=content, metadata=metadata, extractor="office/python-pptx@1")

View File

@@ -31,6 +31,11 @@ EXTRACT_EXTS = {"docx", "pdf", "pptx", "xlsx"}
# Videos are meetings → meetus territory.
MEETING_EXTS = {"mp4", "mkv", "mov", "m4v", "webm", "avi", "wmv"}
# Sidecar suffix meetus writes a meeting's output into. Must match the batch
# invocation `make batch ... OUT_FORMAT="{name}.meetus"` so the index pointer set
# here (deterministically, at index time) resolves once meetus has run.
MEETING_SIDECAR_SUFFIX = ".meetus"
def classify(ext: str) -> str:
if ext in EXTRACT_EXTS:
@@ -40,7 +45,9 @@ def classify(ext: str) -> str:
return "link"
def build_index(source_root, output_dir, options=None, dry_run=False) -> dict:
def build_index(source_root, output_dir, options=None, dry_run=False, only="all") -> dict:
"""Index a source tree. `only` scopes the collection so outputs stay separate:
'all' (default), 'docs' (skip meetings — no bloat), 'meetings' (only videos)."""
options = options or {}
root = Path(source_root).resolve()
if not root.is_dir():
@@ -58,8 +65,12 @@ def build_index(source_root, output_dir, options=None, dry_run=False) -> dict:
rel = p.relative_to(root).as_posix()
ext = p.suffix.lower().lstrip(".")
mode = classify(ext)
# Scope the collection: docs exclude meetings; meetings exclude the rest.
if (only == "docs" and mode == "meeting") or (only == "meetings" and mode != "meeting"):
continue
counts[mode] += 1
stat = p.stat()
birth = getattr(stat, "st_birthtime", None) # not on every filesystem
node = {
"path": rel,
"name": p.name,
@@ -68,16 +79,23 @@ def build_index(source_root, output_dir, options=None, dry_run=False) -> dict:
"mode": mode,
"bytes": stat.st_size,
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
"created": datetime.fromtimestamp(birth or stat.st_mtime).isoformat(),
"url": None, # Drive link — filled later from a URL source
}
if mode == "extracted" and not dry_run:
_extract_sidecar(p, rel, out, options, node, counts)
elif mode == "meeting":
# Deterministic pointer to meetus's output sidecar (produced later by
# `make batch ... OUT_FORMAT="{name}.meetus"`). No relink needed.
node["out"] = rel + MEETING_SIDECAR_SUFFIX
node["transcript"] = f"{rel}{MEETING_SIDECAR_SUFFIX}/{p.stem}_enhanced.txt"
files.append(node)
index = {
"root": str(root),
"only": only,
"generatedAt": datetime.now().isoformat(),
"counts": {**counts, "total": len(files)},
"files": files,
@@ -87,6 +105,14 @@ def build_index(source_root, output_dir, options=None, dry_run=False) -> dict:
json.dumps(index, indent=2, ensure_ascii=False), encoding="utf-8"
)
logger.info("Wrote index: %s (%d files)", out / "index.json", len(files))
# Export ONLY the meeting paths (relative), so a meetus run elsewhere reads
# just what it needs: mount the drive at a new root and feed this list to
# `ctrl/batch.sh -l`. Relative paths re-root cleanly under the new mount.
meetings = [f["path"] for f in files if f["mode"] == "meeting"]
(out / "meetings.txt").write_text(
"".join(m + "\n" for m in meetings), encoding="utf-8"
)
logger.info("Wrote meetings list: %s (%d meetings)", out / "meetings.txt", len(meetings))
return index

View File

@@ -11,10 +11,16 @@ from typing import Dict, Any, Optional
logger = logging.getLogger(__name__)
# Default run-folder name format. Tokens: {date} (YYYYMMDD), {run} (per-day
# counter), {stem} (filename w/o ext), {name} (full filename incl. ext).
DEFAULT_FOLDER_FORMAT = "{date}-{run:03d}-{stem}"
class OutputManager:
"""Manage output directories and manifest files for video processing."""
def __init__(self, video_path: Path, base_output_dir: str = "output", use_cache: bool = True):
def __init__(self, video_path: Path, base_output_dir: str = "output",
use_cache: bool = True, folder_format: str = DEFAULT_FOLDER_FORMAT):
"""
Initialize output manager.
@@ -22,10 +28,15 @@ class OutputManager:
video_path: Path to the video file being processed
base_output_dir: Base directory for all outputs
use_cache: Whether to use existing directories if found
folder_format: Run-folder name template. Tokens: {date}, {run},
{stem}, {name}. When {run} is absent the counter is skipped and
the expanded name is used directly (reuse-by-name = caching), which
is how meetings land in a stable `<file>.meetus/` sidecar.
"""
self.video_path = video_path
self.base_output_dir = Path(base_output_dir)
self.use_cache = use_cache
self.folder_format = folder_format or DEFAULT_FOLDER_FORMAT
# Find or create output directory
self.output_dir = self._get_or_create_output_dir()
@@ -34,16 +45,45 @@ class OutputManager:
logger.info(f"Output directory: {self.output_dir}")
def _has_run(self) -> bool:
"""Whether the folder format uses the per-day {run} counter."""
return "{run" in self.folder_format
def _expand(self, run: int = 1) -> str:
"""Expand the folder-format template to a directory name."""
return self.folder_format.format(
date=datetime.now().strftime("%Y%m%d"),
run=run,
stem=self.video_path.stem,
name=self.video_path.name,
)
def _get_or_create_output_dir(self) -> Path:
"""
Get existing output directory or create a new one with incremental number.
Get existing output directory or create one per the folder format.
With a {run} counter (the default) this reuses the most recent run for
this video when caching, else creates the next-numbered folder. Without
{run} (e.g. "{name}.meetus") the name is deterministic — reuse it when it
exists (caching), else create it.
Returns:
Path to output directory
"""
video_name = self.video_path.stem
# Look for existing directories if caching is enabled
# Deterministic (no {run}) — stable name, reuse-by-name gives caching.
if not self._has_run():
dir_name = self._expand()
output_dir = self.base_output_dir / dir_name
if self.use_cache and output_dir.exists():
logger.info(f"Found existing output: {dir_name}")
else:
output_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"Created new output directory: {dir_name}")
return output_dir
# {run} counter (default): reuse most recent for this video, else next NNN.
if self.use_cache and self.base_output_dir.exists():
existing_dirs = sorted([
d for d in self.base_output_dir.iterdir()
@@ -76,7 +116,7 @@ class OutputManager:
else:
next_run = 1
dir_name = f"{date_str}-{next_run:03d}-{video_name}"
dir_name = self._expand(run=next_run)
output_dir = self.base_output_dir / dir_name
output_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"Created new output directory: {dir_name}")

View File

@@ -9,7 +9,7 @@ import subprocess
import shutil
from typing import Dict, Any, Optional
from .output_manager import OutputManager
from .output_manager import OutputManager, DEFAULT_FOLDER_FORMAT
from .cache_manager import CacheManager
from .frame_extractor import FrameExtractor
from .transcript_merger import TranscriptMerger
@@ -31,6 +31,8 @@ class WorkflowConfig:
self.transcript_path = kwargs.get('transcript')
self.output_dir = kwargs.get('output_dir', 'output')
self.custom_output = kwargs.get('output')
# Run-folder name template (see meetus/output_manager.py). None → default.
self.out_format = kwargs.get('out_format')
# Whisper options
self.run_whisper = kwargs.get('run_whisper', False)
@@ -124,7 +126,8 @@ class ProcessingWorkflow:
self.output_mgr = OutputManager(
config.video_path,
config.output_dir,
use_cache=not config.no_cache
use_cache=not config.no_cache,
folder_format=config.out_format or DEFAULT_FOLDER_FORMAT,
)
self.cache_mgr = CacheManager(
self.output_mgr.output_dir,

View File

@@ -97,6 +97,13 @@ Examples:
help='Base directory for outputs (default: output/)',
default='output'
)
parser.add_argument(
'--out-format',
help='Run-folder name template. Tokens: {date} {run} {stem} {name}. '
'Default: "{date}-{run:03d}-{stem}". Omit {run} for a stable folder '
'(e.g. "{name}.meetus" → a docs-coherent sidecar for doocus).',
default=None
)
# Frame extraction options
parser.add_argument(

View File

@@ -23,6 +23,9 @@ def main() -> int:
parser.add_argument("source", help="Local drive-download root to index")
parser.add_argument("--output", default="docs-output",
help="Output directory for index.json + sidecars (default: docs-output)")
parser.add_argument("--only", choices=["all", "docs", "meetings"], default="all",
help="Scope the collection: 'docs' skips meetings (no bloat), "
"'meetings' indexes only videos (co-locate with .meetus). Default: all")
parser.add_argument("--render", action="store_true",
help="Render page/slide images where supported (needs extra deps)")
parser.add_argument("--ocr", action="store_true",
@@ -41,6 +44,7 @@ def main() -> int:
args.output,
options={"render": args.render, "ocr": args.ocr},
dry_run=args.dry_run,
only=args.only,
)
except NotADirectoryError as e:
print(f"ERROR: {e}", file=sys.stderr)
@@ -51,6 +55,9 @@ def main() -> int:
f"{c['meeting']} meeting, {c['link']} linked"
+ (f", {c['warnings']} with warnings" if c.get("warnings") else "")
+ (" (dry run — nothing written)" if args.dry_run else ""))
if not args.dry_run and c["meeting"]:
print(f"{args.output}/meetings.txt lists the {c['meeting']} meeting path(s) "
f"for meetus: `ctrl/batch.sh -i <mount> -o {args.output} -l {args.output}/meetings.txt`")
return 0

View File

@@ -51,6 +51,10 @@ class TestDefaults(unittest.TestCase):
def test_to_dict_analysis_is_embed_images(self):
self.assertEqual(cfg().to_dict()["analysis"]["method"], "embed-images")
def test_out_format_defaults_none_and_passes_through(self):
self.assertIsNone(cfg().out_format)
self.assertEqual(cfg(out_format="{name}.meetus").out_format, "{name}.meetus")
def test_to_dict_whisper_has_diarize_and_formats(self):
d = cfg(diarize=True, transcript_formats="srt").to_dict()
self.assertTrue(d["whisper"]["diarize"])

View File

@@ -13,6 +13,7 @@ from pathlib import Path
from tests import REPO_ROOT # noqa: F401 (puts repo root on sys.path)
from doocus import registry
from doocus.workflow import DocConfig, DocWorkflow
from doocus.tree import build_index, classify
def has(mod: str) -> bool:
@@ -136,5 +137,71 @@ class TestExtractors(unittest.TestCase):
self.assertEqual(meta["title"], "PDF Doc")
class TestTreeIndex(unittest.TestCase):
def test_classify(self):
self.assertEqual(classify("docx"), "extracted")
self.assertEqual(classify("pdf"), "extracted")
self.assertEqual(classify("mp4"), "meeting")
self.assertEqual(classify("mkv"), "meeting")
self.assertEqual(classify("md"), "link")
self.assertEqual(classify("csv"), "link")
self.assertEqual(classify("bin"), "link")
def test_build_index_replicates_tree(self):
with tempfile.TemporaryDirectory() as tmp:
src = Path(tmp) / "drive"
(src / "team a" / "sub").mkdir(parents=True)
(src / "team a" / "notes.md").write_text("# Hi\n\nbody\n")
(src / "team a" / "sub" / "t.csv").write_text("a,b\n1,2\n")
(src / "team a" / "clip.mp4").write_bytes(b"\x00\x00") # video → meeting
(src / "misc.bin").write_bytes(b"\x00") # unknown → link
out = Path(tmp) / "out"
index = build_index(src, out)
# Whole tree replicated (no flattening), paths preserved.
paths = {f["path"] for f in index["files"]}
self.assertEqual(paths, {
"team a/notes.md", "team a/sub/t.csv", "team a/clip.mp4", "misc.bin"})
modes = {f["path"]: f["mode"] for f in index["files"]}
self.assertEqual(modes["team a/clip.mp4"], "meeting")
self.assertEqual(modes["team a/notes.md"], "link")
self.assertEqual(modes["misc.bin"], "link")
# Every node carries the Drive-url slot and a created date.
self.assertTrue(all("url" in f for f in index["files"]))
self.assertTrue(all(f.get("created") for f in index["files"]))
self.assertTrue((out / "index.json").exists())
def test_meeting_node_points_at_meetus_sidecar(self):
with tempfile.TemporaryDirectory() as tmp:
src = Path(tmp) / "drive"
(src / "team a").mkdir(parents=True)
(src / "team a" / "standup.mp4").write_bytes(b"\x00\x00")
index = build_index(src, Path(tmp) / "out")
node = next(f for f in index["files"] if f["path"] == "team a/standup.mp4")
# Deterministic pointer that `make batch ... OUT_FORMAT="{name}.meetus"` fills.
self.assertEqual(node["out"], "team a/standup.mp4.meetus")
self.assertEqual(node["transcript"], "team a/standup.mp4.meetus/standup_enhanced.txt")
@unittest.skipUnless(has("docx"), "python-docx not installed")
def test_build_index_extracts_sidecar(self):
import docx
with tempfile.TemporaryDirectory() as tmp:
src = Path(tmp) / "drive"
src.mkdir()
doc = docx.Document()
doc.add_paragraph("hello world")
doc.save(src / "report.docx")
out = Path(tmp) / "out"
index = build_index(src, out)
node = next(f for f in index["files"] if f["path"] == "report.docx")
self.assertEqual(node["mode"], "extracted")
sidecar = out / node["out"]
self.assertTrue((sidecar / "content.md").exists())
self.assertTrue((sidecar / "meta.json").exists())
self.assertIn("hello", (sidecar / "content.md").read_text())
if __name__ == "__main__":
unittest.main()

View File

@@ -50,6 +50,15 @@ class TestMakefile(unittest.TestCase):
self.assertEqual(r.returncode, 0, r.stderr)
self.assertIn("--language es", r.stdout)
def test_out_format_forwarded(self):
with tempfile.TemporaryDirectory() as tmp:
ind = Path(tmp) / "in"
ind.mkdir()
(ind / "x.mp4").touch()
r = make("batch", IN=str(ind), PYTHON="echo", OUT_FORMAT="{name}.meetus")
self.assertEqual(r.returncode, 0, r.stderr)
self.assertIn("--out-format {name}.meetus", r.stdout)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,43 @@
"""OutputManager run-folder naming: the default {run} counter and the new
configurable folder_format (deterministic sidecars for the doocus meeting flow)."""
import tempfile
import unittest
from pathlib import Path
from tests import REPO_ROOT # noqa: F401
from meetus.output_manager import OutputManager, DEFAULT_FOLDER_FORMAT
class TestFolderFormat(unittest.TestCase):
def test_default_uses_date_run_stem(self):
with tempfile.TemporaryDirectory() as d:
om = OutputManager(Path("/x/meeting.mkv"), d)
name = om.output_dir.name
self.assertTrue(name.endswith("-meeting"))
# YYYYMMDD-NNN-meeting → middle part is the zero-padded run number
self.assertRegex(name, r"^\d{8}-\d{3}-meeting$")
def test_deterministic_sidecar_no_counter(self):
with tempfile.TemporaryDirectory() as d:
om = OutputManager(Path("/x/team a/standup.mp4"), d, folder_format="{name}.meetus")
self.assertEqual(om.output_dir.name, "standup.mp4.meetus")
def test_sidecar_reused_by_name(self):
with tempfile.TemporaryDirectory() as d:
a = OutputManager(Path("/x/standup.mp4"), d, folder_format="{name}.meetus")
b = OutputManager(Path("/x/standup.mp4"), d, folder_format="{name}.meetus")
self.assertEqual(a.output_dir, b.output_dir) # caching by stable name
def test_stem_token(self):
with tempfile.TemporaryDirectory() as d:
om = OutputManager(Path("/x/standup.mp4"), d, folder_format="{stem}")
self.assertEqual(om.output_dir.name, "standup")
def test_empty_format_falls_back_to_default(self):
with tempfile.TemporaryDirectory() as d:
om = OutputManager(Path("/x/m.mkv"), d, folder_format="")
self.assertEqual(om.folder_format, DEFAULT_FOLDER_FORMAT)
if __name__ == "__main__":
unittest.main()

View File

@@ -8,8 +8,12 @@
"name": "doocus-browser-ui",
"version": "0.1.0",
"dependencies": {
"highlight.js": "^11.10",
"jszip": "^3.10",
"vue": "^3.5"
"mammoth": "^1.8",
"marked": "^14",
"vue": "^3.5",
"xlsx": "^0.18"
},
"devDependencies": {
"@types/node": "^22",
@@ -1059,6 +1063,24 @@
"integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==",
"license": "MIT"
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.13",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/alien-signals": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
@@ -1066,6 +1088,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1073,6 +1104,32 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
@@ -1083,12 +1140,46 @@
"balanced-match": "^1.0.0"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -1102,6 +1193,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/dingbat-to-unicode": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
"license": "BSD-2-Clause"
},
"node_modules/duck": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
"license": "BSD",
"dependencies": {
"underscore": "^1.13.1"
}
},
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
@@ -1180,6 +1286,15 @@
}
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1205,6 +1320,15 @@
"he": "bin/he"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
@@ -1244,6 +1368,17 @@
"immediate": "~3.0.5"
}
},
"node_modules/lop": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
"license": "BSD-2-Clause",
"dependencies": {
"duck": "^0.1.12",
"option": "~0.2.1",
"underscore": "^1.13.1"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -1253,6 +1388,42 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/mammoth": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz",
"integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==",
"license": "BSD-2-Clause",
"dependencies": {
"@xmldom/xmldom": "^0.8.6",
"argparse": "~1.0.3",
"base64-js": "^1.5.1",
"bluebird": "~3.4.0",
"dingbat-to-unicode": "^1.0.1",
"jszip": "^3.7.1",
"lop": "^0.4.2",
"path-is-absolute": "^1.0.0",
"underscore": "^1.13.1",
"xmlbuilder": "^10.0.0"
},
"bin": {
"mammoth": "bin/mammoth"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/marked": {
"version": "14.1.4",
"resolved": "https://registry.npmjs.org/marked/-/marked-14.1.4.tgz",
"integrity": "sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
@@ -1294,6 +1465,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/option": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
"license": "BSD-2-Clause"
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@@ -1307,6 +1484,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1441,6 +1627,24 @@
"node": ">=0.10.0"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@@ -1481,6 +1685,12 @@
"node": ">=14.17"
}
},
"node_modules/underscore": {
"version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -1613,6 +1823,54 @@
"peerDependencies": {
"typescript": ">=5.0.0"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/xmlbuilder": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
}
}
}

View File

@@ -10,8 +10,12 @@
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"highlight.js": "^11.10",
"jszip": "^3.10",
"vue": "^3.5"
"mammoth": "^1.8",
"marked": "^14",
"vue": "^3.5",
"xlsx": "^0.18"
},
"devDependencies": {
"@types/node": "^22",

View File

@@ -1,217 +1,498 @@
/**
* Vite dev middleware exposing the local doocus docs-output tree to the app.
* Vite dev middleware exposing one or more local doocus output trees.
*
* Runs in the dev server process (Node), so it can reach the original source
* file by the absolute path stored in each doc's manifest.json — needed for the
* package builder, which bundles the original alongside content.md + meta.json.
* All reads are local; nothing leaves the machine.
* Each output dir is a "collection" (its own index.json + sidecars + source
* root). The client can turn collections on/off; file paths are composite —
* "<collectionId>/<relpath>" — so a single path identifies both the collection
* and the file within it. All reads are local; nothing leaves the machine.
*
* Routes:
* GET /api/docs list extracted docs
* GET /api/docs/:id meta.json + content.md + thumb/original flags
* GET /api/docs/:id/thumb stream thumb.jpg
* GET /api/docs/:id/original stream the original source file
* Routes (path params are composite, query is ?path=/?q=):
* GET /api/collections available collections
* GET /api/tree?collections=a,b merged tree of the active collections
* GET /api/detail?path=<c/rel> node + extracted content + meta + text
* GET /api/original?path=<c/rel> stream the original (Range-enabled)
* GET /api/meeting?path=<c/rel> meetus review data for a meeting
* GET /api/meeting/frame?path=&file= a meeting frame
* GET /api/runs/:cid (cid=c/rel) meetus-shaped run (for the embed)
* PUT /api/runs/:cid/review write the meeting's review.json
* GET /api/search?q=&collections=a,b search cached text across active cols
*/
import type { Plugin } from 'vite'
import type { IncomingMessage, ServerResponse } from 'node:http'
import { spawn } from 'node:child_process'
import fs from 'node:fs'
import fsp from 'node:fs/promises'
import path from 'node:path'
interface Options {
outputDir: string
outputDirs: string[] // explicit output dirs (env)
outputsRoots: string[] // managed roots (doocus-data, meetus-data): <root>/<source>/
repoRoot: string // where process_tree.py lives (run via uv)
}
interface Manifest {
source?: { name?: string; path?: string; sha256?: string }
processed_at?: string
outputs?: { content?: string; meta?: string; thumb?: string | null }
interface Collection {
id: string
name: string
dir: string
indexPath: string
}
const ORIGINAL_MIME: Record<string, string> = {
'.pdf': 'application/pdf',
interface Node {
path: string
name: string
ext: string
family: string
mode: 'extracted' | 'meeting' | 'link'
bytes: number
modified: string
url: string | null
out?: string
title?: string
warnings?: string[]
}
interface Index {
root: string
files: Node[]
counts?: Record<string, number>
}
const TEXT_EXTS = new Set(['md', 'markdown', 'txt', 'json', 'yaml', 'yml', 'csv', 'html', 'htm'])
const MIME: Record<string, string> = {
'.pdf': 'application/pdf', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
'.html': 'text/html', '.htm': 'text/html', '.md': 'text/markdown', '.txt': 'text/plain',
'.csv': 'text/csv', '.json': 'application/json', '.yaml': 'text/yaml', '.yml': 'text/yaml',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'.csv': 'text/csv',
'.html': 'text/html',
'.htm': 'text/html',
'.json': 'application/json',
'.md': 'text/markdown',
'.txt': 'text/plain',
'.yaml': 'text/yaml',
'.yml': 'text/yaml',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.mp4': 'video/mp4',
}
function buildCollections(dirs: string[]): Collection[] {
const seen = new Map<string, number>()
return dirs.map((d) => {
const dir = path.resolve(d)
let id = path.basename(dir) || 'output'
const n = seen.get(id) ?? 0
seen.set(id, n + 1)
if (n > 0) id = `${id}-${n + 1}`
return { id, name: id, dir, indexPath: path.join(dir, 'index.json') }
})
}
export function doocusApi(opts: Options): Plugin {
const outputDir = path.resolve(opts.outputDir)
const toPosix = (p: string) => p.split(path.sep).join('/')
/** Resolve a doc id (possibly nested) to its directory, rejecting traversal. */
function docDir(id: string): string | null {
const dir = path.resolve(outputDir, id)
if (dir !== outputDir && !dir.startsWith(outputDir + path.sep)) return null
try {
if (fs.statSync(dir).isDirectory()) return dir
} catch { /* missing */ }
return null
}
/** Find every doc run directory (one containing meta.json) under `base`. */
async function findDocDirs(base: string, maxDepth = 8): Promise<string[]> {
const found: string[] = []
const walk = async (dir: string, depth: number): Promise<void> => {
if (depth > maxDepth) return
let ents
try {
ents = await fsp.readdir(dir, { withFileTypes: true })
} catch { return }
if (ents.some((e) => e.isFile() && e.name === 'meta.json')) {
found.push(dir)
return // a doc dir; don't descend into assets/
}
for (const e of ents) {
if (!e.isDirectory()) continue
if (e.name === 'assets' || e.name === 'node_modules' || e.name.startsWith('.')) continue
await walk(path.join(dir, e.name), depth + 1)
/** Collections discovered across the managed roots (each is <root>/<source>/). */
function discoverInRoots(): Collection[] {
const found: Collection[] = []
for (const root of opts.outputsRoots) {
let subs: fs.Dirent[] = []
try { subs = fs.readdirSync(root, { withFileTypes: true }) } catch { continue }
for (const d of subs) {
if (d.isDirectory() && fs.existsSync(path.join(root, d.name, 'index.json'))) {
found.push({
id: d.name, name: d.name,
dir: path.join(root, d.name),
indexPath: path.join(root, d.name, 'index.json'),
})
}
}
}
await walk(base, 0)
return found
}
function readJson<T>(file: string): T | null {
try {
return JSON.parse(fs.readFileSync(file, 'utf-8')) as T
} catch {
return null
/** Current collections = explicit env dirs + managed-root subdirs (deduped,
* recomputed each request so UI scans appear without a restart). */
function currentCollections(): Collection[] {
const out: Collection[] = []
const seen = new Set<string>()
for (const c of [...buildCollections(opts.outputDirs), ...discoverInRoots()]) {
if (out.some((o) => o.dir === c.dir)) continue
let id = c.id, n = 1
while (seen.has(id)) { n++; id = `${c.id}-${n}` }
seen.add(id)
out.push({ ...c, id })
}
return out
}
function collectionById(id: string): Collection | undefined {
return currentCollections().find((c) => c.id === id)
}
function readIndexFor(c: Collection): Index | null {
try { return JSON.parse(fs.readFileSync(c.indexPath, 'utf-8')) } catch { return null }
}
/** Split a composite "<collectionId>/<rel>" into its collection + rel path. */
function splitPath(p: string): { col: Collection; rel: string } | null {
const i = p.indexOf('/')
const id = i < 0 ? p : p.slice(0, i)
const rel = i < 0 ? '' : p.slice(i + 1)
const col = collectionById(id)
return col ? { col, rel } : null
}
/** Resolve a file within its collection's source root, rejecting traversal. */
function originalAbs(index: Index, rel: string): string | null {
const root = path.resolve(index.root)
const abs = path.resolve(root, rel)
if (abs !== root && !abs.startsWith(root + path.sep)) return null
return abs
}
function meetusDir(col: Collection, rel: string): { dir: string; stem: string } {
return { dir: path.join(col.dir, rel + '.meetus'), stem: path.parse(rel).name }
}
// Per-collection search index over cached text — rebuilt on index.json change.
const searchCache = new Map<string, { mtime: number; entries: Array<{ path: string; hay: string; bytes: number }> }>()
async function readCap(p: string | null, cap = 20_000_000): Promise<string> {
if (!p) return ''
try { return (await fsp.readFile(p, 'utf-8')).slice(0, cap) } catch { return '' }
}
async function buildSearchIndex(c: Collection): Promise<void> {
const index = readIndexFor(c)
let mtime = 0
try { mtime = fs.statSync(c.indexPath).mtimeMs } catch { /* */ }
const entries: Array<{ path: string; hay: string; bytes: number }> = []
for (const f of index?.files ?? []) {
let text = `${f.path} ${f.title ?? ''}`
if (f.mode === 'extracted' && f.out) {
text += ' ' + await readCap(path.join(c.dir, f.out, 'content.md'))
} else if (f.mode === 'meeting') {
const { dir, stem } = meetusDir(c, f.path)
text += ' ' + await readCap(path.join(dir, `${stem}_enhanced.txt`))
} else if (TEXT_EXTS.has(f.ext) && index) {
text += ' ' + await readCap(originalAbs(index, f.path))
}
entries.push({ path: `${c.id}/${f.path}`, hay: text.toLowerCase(), bytes: f.bytes })
}
searchCache.set(c.id, { mtime, entries })
}
return {
name: 'doocus-api',
configureServer(server) {
server.config.logger.info(`[doocus-api] serving docs from: ${outputDir}`)
const cols = currentCollections()
server.config.logger.info(
`[doocus-api] ${cols.length} collection(s); managed roots: ${opts.outputsRoots.join(', ')}`)
server.middlewares.use('/api', (req, res, next) => {
const url = new URL(req.url ?? '/', 'http://localhost')
const parts = url.pathname.split('/').filter(Boolean)
handle(req, res, parts).catch((err) => {
handle(req, res, parts, url.searchParams).catch((err) => {
server.config.logger.error(`[doocus-api] ${String(err)}`)
sendJson(res, 500, { error: String(err) })
}).then((handled) => {
if (!handled) next()
})
}).then((handled) => { if (!handled) next() })
})
},
}
async function handle(req: IncomingMessage, res: ServerResponse, parts: string[]): Promise<boolean> {
if (parts[0] !== 'docs') return false
// GET /api/docs
if (parts.length === 1 && req.method === 'GET') {
await listDocs(res)
return true
async function handle(req: IncomingMessage, res: ServerResponse, parts: string[], q: URLSearchParams): Promise<boolean> {
if (parts[0] === 'collections') { listCollections(res); return true }
if (parts[0] === 'scan' && req.method === 'POST') { await runScan(req, res, false); return true }
if (parts[0] === 'rescan' && req.method === 'POST') { await runScan(req, res, true); return true }
if (parts[0] === 'tree') { getTree(res, q.get('collections')); return true }
if (parts[0] === 'detail') { await getDetail(res, q.get('path') ?? ''); return true }
if (parts[0] === 'original') { await serveOriginal(res, q.get('path') ?? '', req.headers.range); return true }
if (parts[0] === 'meeting' && parts[1] === 'frame') { serveMeetingFrame(res, q.get('path') ?? '', q.get('file') ?? ''); return true }
if (parts[0] === 'meeting') { await getMeeting(res, q.get('path') ?? ''); return true }
if (parts[0] === 'search') { await search(res, q.get('q') ?? '', q.get('collections')); return true }
if (parts[0] === 'runs' && parts.length >= 2) {
const id = decodeURIComponent(parts[1])
if (parts.length === 2 && req.method === 'GET') { await getRunShaped(res, id); return true }
if (parts.length === 3 && parts[2] === 'review' && req.method === 'PUT') { await saveRunReview(req, res, id); return true }
}
const id = parts[1] ? decodeURIComponent(parts[1]) : ''
const dir = id ? docDir(id) : null
if (!dir) { sendJson(res, 404, { error: 'doc not found' }); return true }
// GET /api/docs/:id
if (parts.length === 2 && req.method === 'GET') {
await getDoc(res, id, dir)
return true
}
// GET /api/docs/:id/thumb
if (parts.length === 3 && parts[2] === 'thumb' && req.method === 'GET') {
serveThumb(res, dir)
return true
}
// GET /api/docs/:id/original
if (parts.length === 3 && parts[2] === 'original' && req.method === 'GET') {
serveOriginal(res, dir)
return true
}
sendJson(res, 404, { error: 'not found' })
return true
return false
}
async function listDocs(res: ServerResponse): Promise<void> {
const dirs = await findDocDirs(outputDir)
const docs = []
for (const dir of dirs) {
const meta = readJson<Record<string, unknown>>(path.join(dir, 'meta.json'))
if (!meta) continue
const id = toPosix(path.relative(outputDir, dir))
const source = (meta.source ?? {}) as { name?: string; bytes?: number }
docs.push({
id,
name: source.name ?? id,
type: meta.type ?? null,
family: meta.family ?? null,
title: meta.title ?? null,
extractedAt: meta.extracted_at ?? null,
wordCount: meta.word_count ?? 0,
bytes: source.bytes ?? 0,
hasThumb: fs.existsSync(path.join(dir, 'thumb.jpg')),
warnings: Array.isArray(meta.warnings) ? meta.warnings.length : 0,
})
}
docs.sort((a, b) =>
String(b.extractedAt ?? '').localeCompare(String(a.extractedAt ?? '')) || a.id.localeCompare(b.id))
sendJson(res, 200, { docs, outputDir })
function listCollections(res: ServerResponse): void {
const out = currentCollections().map((c) => {
const idx = readIndexFor(c)
const meetings = idx?.files.filter((f) => f.mode === 'meeting').length ?? 0
return {
id: c.id, name: c.name, available: !!idx,
count: idx?.files.length ?? 0,
root: idx?.root ?? null, // shared root → same source
only: (idx as any)?.only ?? 'all',
meetings,
}
})
sendJson(res, 200, { collections: out })
}
async function getDoc(res: ServerResponse, id: string, dir: string): Promise<void> {
const meta = readJson<Record<string, unknown>>(path.join(dir, 'meta.json'))
if (!meta) { sendJson(res, 404, { error: 'meta missing' }); return }
const manifest = readJson<Manifest>(path.join(dir, 'manifest.json'))
/** Run process_tree.py over a source folder (new scan) or an existing
* collection's recorded source (rescan), writing to the managed root. */
async function runScan(req: IncomingMessage, res: ServerResponse, rerun: boolean): Promise<void> {
let body: { source?: string; id?: string }
try { body = JSON.parse((await readBody(req)) || '{}') } catch { sendJson(res, 400, { error: 'invalid JSON' }); return }
let source: string
let dir: string
let only = 'docs' // UI scans create document collections (meetings come from the batch)
if (rerun) {
const col = body.id ? collectionById(body.id) : undefined
const idx = col ? readIndexFor(col) : null
if (!col || !idx?.root) { sendJson(res, 404, { error: 'collection not found' }); return }
source = idx.root
dir = col.dir
only = (idx as any).only ?? 'all' // preserve the collection's kind on rescan
} else {
source = (body.source ?? '').trim()
if (!source || !isDir(source)) { sendJson(res, 400, { error: `not a directory: ${source}` }); return }
const slug = path.basename(path.resolve(source)).replace(/[^\w.-]+/g, '_') || 'source'
dir = path.join(opts.outputsRoots[0], slug) // docs land in the first root (doocus-data)
fs.mkdirSync(opts.outputsRoots[0], { recursive: true })
}
let content = ''
try {
content = await fsp.readFile(path.join(dir, 'content.md'), 'utf-8')
} catch { /* none */ }
await runProcessTree(source, dir, only)
sendJson(res, 200, { ok: true, id: path.basename(dir) })
} catch (e) {
sendJson(res, 500, { error: String(e) })
}
}
const hasThumb = fs.existsSync(path.join(dir, 'thumb.jpg'))
const originalPath = manifest?.source?.path
const hasOriginal = !!originalPath && isFile(originalPath)
sendJson(res, 200, {
id,
meta,
content,
hasThumb,
thumbUrl: hasThumb ? `/api/docs/${encodeURIComponent(id)}/thumb` : null,
hasOriginal,
originalUrl: hasOriginal ? `/api/docs/${encodeURIComponent(id)}/original` : null,
/** Spawn `uv run python process_tree.py <source> --output <dir> --only <only>`. */
function runProcessTree(source: string, dir: string, only: string): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn('uv',
['run', 'python', 'process_tree.py', source, '--output', dir, '--only', only],
{ cwd: opts.repoRoot })
let err = ''
child.stderr.on('data', (d) => { err += d })
child.on('error', reject)
child.on('close', (code) => code === 0 ? resolve() : reject(new Error(err || `exit ${code}`)))
})
}
function serveThumb(res: ServerResponse, dir: string): void {
const full = path.join(dir, 'thumb.jpg')
if (!fs.existsSync(full)) { sendJson(res, 404, { error: 'no thumb' }); return }
/** Higher = "richer" version of a file — the one whose sidecar resolves wins,
* so a meetus-data meeting (with .meetus) overrides a doocus copy without it. */
function scoreNode(c: Collection, f: Node): number {
if (f.mode === 'meeting') return fs.existsSync(meetusDir(c, f.path).dir) ? 4 : 2
if (f.mode === 'extracted') {
return f.out && fs.existsSync(path.join(c.dir, f.out, 'content.md')) ? 3 : 1.5
}
return 1
}
function getTree(res: ServerResponse, activeCsv: string | null): void {
const active = activeCsv ? new Set(activeCsv.split(',').filter(Boolean)) : null
const cols = currentCollections().filter((c) => !active || active.has(c.id))
// Dedup across collections by shared root + rel; the resolving copy wins.
const best = new Map<string, { file: Node & { collection: string; root: string; rel: string }; score: number }>()
for (const c of cols) {
const idx = readIndexFor(c)
if (!idx) continue
for (const f of idx.files) {
const key = `${idx.root}${f.path}`
const score = scoreNode(c, f)
const cur = best.get(key)
if (!cur || score > cur.score) {
best.set(key, {
file: { ...f, collection: c.id, root: idx.root, rel: f.path, path: `${c.id}/${f.path}` },
score,
})
}
}
}
const files = [...best.values()].map((v) => v.file)
const counts: Record<string, number> = { extracted: 0, meeting: 0, link: 0, total: files.length }
for (const f of files) counts[f.mode] = (counts[f.mode] ?? 0) + 1
sendJson(res, 200, {
collections: currentCollections().map((c) => ({ id: c.id, name: c.name })),
files,
counts,
})
}
async function getDetail(res: ServerResponse, composite: string): Promise<void> {
const sp = splitPath(composite)
if (!sp) { sendJson(res, 404, { error: 'collection not found' }); return }
const idx = readIndexFor(sp.col)
const node = idx?.files.find((f) => f.path === sp.rel)
if (!idx || !node) { sendJson(res, 404, { error: 'node not found' }); return }
let content: string | null = null
let meta: unknown = null
let text: string | null = null
if (node.mode === 'extracted' && node.out) {
try { content = await fsp.readFile(path.join(sp.col.dir, node.out, 'content.md'), 'utf-8') } catch { /* */ }
try { meta = JSON.parse(await fsp.readFile(path.join(sp.col.dir, node.out, 'meta.json'), 'utf-8')) } catch { /* */ }
} else if (node.mode === 'link' && TEXT_EXTS.has(node.ext)) {
const abs = originalAbs(idx, sp.rel)
if (abs) { try { text = await fsp.readFile(abs, 'utf-8') } catch { /* */ } }
}
sendJson(res, 200, {
node: { ...node, collection: sp.col.id, path: composite },
content, meta, text,
mime: MIME[path.extname(node.name).toLowerCase()] ?? 'application/octet-stream',
originalUrl: `/api/original?path=${encodeURIComponent(composite)}`,
})
}
async function serveOriginal(res: ServerResponse, composite: string, range?: string): Promise<void> {
const sp = splitPath(composite)
const idx = sp ? readIndexFor(sp.col) : null
const abs = sp && idx ? originalAbs(idx, sp.rel) : null
if (!abs || !isFile(abs)) { sendJson(res, 404, { error: 'original not reachable' }); return }
const mime = MIME[path.extname(abs).toLowerCase()] ?? 'application/octet-stream'
const size = fs.statSync(abs).size
res.setHeader('Content-Type', mime)
res.setHeader('Accept-Ranges', 'bytes')
const m = range?.match(/bytes=(\d*)-(\d*)/)
if (m) {
const start = m[1] ? parseInt(m[1], 10) : 0
const end = m[2] ? parseInt(m[2], 10) : size - 1
if (start >= size || end >= size || start > end) {
res.statusCode = 416; res.setHeader('Content-Range', `bytes */${size}`); res.end(); return
}
res.statusCode = 206
res.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
res.setHeader('Content-Length', String(end - start + 1))
fs.createReadStream(abs, { start, end }).pipe(res)
return
}
res.setHeader('Content-Length', String(size))
fs.createReadStream(abs).pipe(res)
}
interface MeetingData {
segments: Array<{ start: number; end: number; text: string; speaker: string | null }>
frames: Array<{ file: string; url: string; time: number | null; size: number }>
enhancedAvailable: boolean
}
async function meetingData(composite: string): Promise<MeetingData | null> {
const sp = splitPath(composite)
if (!sp) return null
const { dir, stem } = meetusDir(sp.col, sp.rel)
const segments: MeetingData['segments'] = []
const whisper = path.join(dir, `${stem}.json`)
if (fs.existsSync(whisper)) {
try {
const data = JSON.parse(await fsp.readFile(whisper, 'utf-8'))
for (const s of (Array.isArray(data) ? data : data.segments ?? [])) {
segments.push({
start: Number(s.start ?? 0), end: Number(s.end ?? s.start ?? 0),
text: String(s.text ?? '').trim(), speaker: s.speaker ?? null,
})
}
} catch { /* none */ }
}
const enhanced = path.join(dir, `${stem}_enhanced.txt`)
const times = new Map<string, number>()
let enhancedAvailable = false
if (fs.existsSync(enhanced)) {
try {
enhancedAvailable = true
let last: number | null = null
for (const line of (await fsp.readFile(enhanced, 'utf-8')).split('\n')) {
const ts = line.match(/^\[(\d+):(\d+)\]/)
if (ts) last = Number(ts[1]) * 60 + Number(ts[2])
const fm = line.match(/Frame:\s*\S*?([^/\\\s]+\.jpg)/i)
if (fm && last != null) times.set(fm[1], last)
}
} catch { /* none */ }
}
const frames: MeetingData['frames'] = []
try {
const framesDir = path.join(dir, 'frames')
for (const file of (await fsp.readdir(framesDir)).filter((f) => f.toLowerCase().endsWith('.jpg')).sort()) {
const fny = file.match(/_(\d+(?:\.\d+)?)s\.jpg$/i)
let size = 0
try { size = (await fsp.stat(path.join(framesDir, file))).size } catch { /* */ }
frames.push({
file,
url: `/api/meeting/frame?path=${encodeURIComponent(composite)}&file=${encodeURIComponent(file)}`,
time: fny ? Number(fny[1]) : times.get(file) ?? null,
size,
})
}
} catch { /* no frames */ }
return { segments, frames, enhancedAvailable }
}
async function getMeeting(res: ServerResponse, composite: string): Promise<void> {
const sp = splitPath(composite)
const data = await meetingData(composite)
if (!sp || !data) { sendJson(res, 404, { error: 'meeting not found' }); return }
sendJson(res, 200, {
path: composite,
hasOutput: fs.existsSync(meetusDir(sp.col, sp.rel).dir),
...data,
videoUrl: `/api/original?path=${encodeURIComponent(composite)}`,
})
}
async function getRunShaped(res: ServerResponse, id: string): Promise<void> {
const sp = splitPath(id)
const data = await meetingData(id)
if (!sp || !data) { sendJson(res, 404, { error: 'run not found' }); return }
const { dir } = meetusDir(sp.col, sp.rel)
let review: unknown = null
try { review = JSON.parse(await fsp.readFile(path.join(dir, 'review.json'), 'utf-8')) } catch { /* */ }
sendJson(res, 200, {
id,
manifest: { video: { name: path.basename(sp.rel) } },
segments: data.segments,
frames: data.frames,
enhancedAvailable: data.enhancedAvailable,
hasVideo: true,
videoUrl: `/api/original?path=${encodeURIComponent(id)}`,
review,
})
}
async function saveRunReview(req: IncomingMessage, res: ServerResponse, id: string): Promise<void> {
const sp = splitPath(id)
if (!sp) { sendJson(res, 404, { error: 'run not found' }); return }
const { dir } = meetusDir(sp.col, sp.rel)
if (!fs.existsSync(dir)) { sendJson(res, 404, { error: 'no meetus output for this meeting' }); return }
let parsed: unknown
try { parsed = JSON.parse((await readBody(req)) || '{}') } catch { sendJson(res, 400, { error: 'invalid JSON' }); return }
const tmp = path.join(dir, `.review.${process.pid}.tmp`)
await fsp.writeFile(tmp, JSON.stringify(parsed, null, 2), 'utf-8')
await fsp.rename(tmp, path.join(dir, 'review.json'))
sendJson(res, 200, { ok: true })
}
function serveMeetingFrame(res: ServerResponse, composite: string, file: string): void {
const sp = splitPath(composite)
if (!sp) { sendJson(res, 404, { error: 'not found' }); return }
const { dir } = meetusDir(sp.col, sp.rel)
const safe = path.basename(file)
const full = path.join(dir, 'frames', safe)
if (safe !== file || !fs.existsSync(full)) { sendJson(res, 404, { error: 'frame not found' }); return }
res.setHeader('Content-Type', 'image/jpeg')
res.setHeader('Cache-Control', 'no-cache')
fs.createReadStream(full).pipe(res)
}
function serveOriginal(res: ServerResponse, dir: string): void {
const manifest = readJson<Manifest>(path.join(dir, 'manifest.json'))
const original = manifest?.source?.path
if (!original || !isFile(original)) {
sendJson(res, 404, { error: 'original source not reachable on this machine' })
return
async function search(res: ServerResponse, query: string, activeCsv: string | null): Promise<void> {
const qq = query.trim().toLowerCase()
const active = activeCsv ? new Set(activeCsv.split(',').filter(Boolean)) : null
const cols = currentCollections().filter((c) => !active || active.has(c.id))
let matches: string[] = []
let bytes = 0
if (qq) {
for (const c of cols) {
let cur = 0
try { cur = fs.statSync(c.indexPath).mtimeMs } catch { /* */ }
const cached = searchCache.get(c.id)
if (!cached || cached.mtime !== cur) await buildSearchIndex(c)
for (const e of searchCache.get(c.id)?.entries ?? []) {
if (e.hay.includes(qq)) { matches.push(e.path); bytes += e.bytes }
}
}
}
const mime = ORIGINAL_MIME[path.extname(original).toLowerCase()] ?? 'application/octet-stream'
res.setHeader('Content-Type', mime)
res.setHeader('Content-Disposition', `attachment; filename="${path.basename(original)}"`)
fs.createReadStream(original).pipe(res)
sendJson(res, 200, { query: qq, matches, count: matches.length, bytes })
}
}
@@ -226,3 +507,16 @@ function sendJson(res: ServerResponse, status: number, obj: unknown): void {
function isFile(p: string): boolean {
try { return fs.statSync(p).isFile() } catch { return false }
}
function isDir(p: string): boolean {
try { return fs.statSync(p).isDirectory() } catch { return false }
}
function readBody(req: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let data = ''
req.on('data', (c) => { data += c; if (data.length > 5_000_000) reject(new Error('body too large')) })
req.on('end', () => resolve(data))
req.on('error', reject)
})
}

View File

@@ -1,71 +1,189 @@
<script setup lang="ts">
import { onMounted, computed } from 'vue'
import Panel from '@framework/components/Panel.vue'
import SplitPane from '@framework/components/SplitPane.vue'
import DocPicker from '@/components/DocPicker.vue'
import DocViewer from '@/components/DocViewer.vue'
import MetadataPanel from '@/components/MetadataPanel.vue'
import FileTree from '@/components/FileTree.vue'
import DocDetail from '@/components/DocDetail.vue'
import ExportBar from '@/components/ExportBar.vue'
import { ref } from 'vue'
import { store, loadTree, runSearch, visibleTree, selectMatches, humanBytes, sources, sourceActive, toggleSource, scanFolder, refreshSources } from '@/store'
onMounted(loadTree)
const showCols = ref(false)
const scanPath = ref('')
async function doScan() {
const p = scanPath.value
if (!p.trim()) return
await scanFolder(p)
if (!store.scanError) scanPath.value = ''
}
const detailTitle = computed(() =>
store.detail?.node.mode === 'meeting' ? 'Meeting' : 'File')
const barLabel = computed(() => store.detail?.node.name ?? 'viewer')
function onSearch(e: Event) {
runSearch((e.target as HTMLInputElement).value)
}
</script>
<template>
<div class="app">
<header class="topbar">
<span class="brand">doocus</span>
<span class="spacer" />
<div class="collections">
<button
class="col-btn"
:title="`Sources — ${sources.length}`"
@click="showCols = !showCols"
></button>
<div v-if="showCols" class="col-menu">
<div class="col-title">
Sources (docs + meetings share a source)
<button class="refresh" title="Re-discover newly-scanned sources" @click="refreshSources"></button>
</div>
<div v-for="s in sources" :key="s.root" class="col-row">
<label>
<input
type="checkbox"
:checked="sourceActive(s.root)"
@change="toggleSource(s.root)"
/>
<span class="col-name" :title="s.root">{{ s.label }}</span>
<span class="dim">{{ s.docs }} docs · {{ s.meetings }} mtg</span>
</label>
</div>
<p v-if="!sources.length" class="dim">No sources yet.</p>
<div class="col-scan">
<input
v-model="scanPath"
class="scan-input"
placeholder="/path/to/source folder"
@keyup.enter="doScan"
/>
<button :disabled="store.scanning || !scanPath.trim()" @click="doScan">
{{ store.scanning ? 'Scanning' : 'Scan' }}
</button>
</div>
<p v-if="store.scanError" class="scan-err">{{ store.scanError }}</p>
</div>
</div>
<div class="search">
<input
class="search-input"
type="search"
:value="store.query"
placeholder="Search transcripts, documents, files…"
@input="onSearch"
/>
<span v-if="store.query" class="search-stats">
<template v-if="store.searching">searching</template>
<template v-else>
{{ store.searchStats.count }} files · {{ humanBytes(store.searchStats.bytes) }}
<button class="pkg" :disabled="!store.searchStats.count" title="Select matches for a package" @click="selectMatches">
package
</button>
</template>
</span>
</div>
<ExportBar />
</header>
<main class="body">
<SplitPane :initial-size="0.9" :min="0.5" :max="2">
<div v-if="store.error" class="err">{{ store.error }}</div>
<SplitPane v-else-if="!store.treeCollapsed && !store.detailCollapsed"
direction="vertical" :initial-size="0.9" :min="0.2" :max="4">
<template #first>
<Panel title="Documents">
<DocPicker />
<Panel title="Tree">
<template #actions><button class="chev" title="Collapse tree" @click="store.treeCollapsed = true"></button></template>
<FileTree :items="visibleTree" />
</Panel>
</template>
<template #second>
<SplitPane :initial-size="2.2" :min="1" :max="5">
<template #first>
<Panel title="Content">
<DocViewer />
</Panel>
</template>
<template #second>
<Panel title="Metadata">
<MetadataPanel />
</Panel>
</template>
</SplitPane>
<Panel :title="detailTitle">
<template #actions><button class="chev" title="Collapse viewer" @click="store.detailCollapsed = true"></button></template>
<DocDetail />
</Panel>
</template>
</SplitPane>
<div v-else class="stack">
<button v-if="store.treeCollapsed" class="bar" title="Expand tree" @click="store.treeCollapsed = false">
<span>Tree · {{ store.counts.total }} files</span>
</button>
<Panel v-else title="Tree" class="grow">
<template #actions><button class="chev" title="Collapse tree" @click="store.treeCollapsed = true"></button></template>
<FileTree :items="visibleTree" />
</Panel>
<Panel v-if="!store.detailCollapsed" :title="detailTitle" class="grow">
<template #actions><button class="chev" title="Collapse viewer" @click="store.detailCollapsed = true"></button></template>
<DocDetail />
</Panel>
<button v-else class="bar" title="Expand viewer" @click="store.detailCollapsed = false">
<span>{{ barLabel }}</span>
</button>
</div>
</main>
</div>
</template>
<style scoped>
.app {
display: flex;
flex-direction: column;
height: 100%;
}
.app { display: flex; flex-direction: column; height: 100%; }
.topbar {
display: flex;
align-items: center;
gap: var(--space-3);
display: flex; align-items: center; gap: var(--space-3);
padding: var(--space-2) var(--space-3);
background: var(--surface-1);
border-bottom: var(--panel-border);
flex-shrink: 0;
background: var(--surface-1); border-bottom: var(--panel-border); flex-shrink: 0;
}
.brand {
font-weight: 700;
letter-spacing: 0.02em;
.brand { font-weight: 700; letter-spacing: 0.02em; flex-shrink: 0; }
.collections { position: relative; flex-shrink: 0; }
.col-btn { padding: var(--space-1) var(--space-2); font-size: 15px; line-height: 1; }
.col-menu {
position: absolute; top: 110%; left: 0; z-index: 20; min-width: 220px;
background: var(--surface-1); border: var(--panel-border); border-radius: var(--panel-radius);
padding: var(--space-2); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
}
.spacer {
flex: 1;
.col-title { font-size: 11px; color: var(--text-secondary); margin-bottom: var(--space-1); display: flex; align-items: center; justify-content: space-between; }
.refresh { padding: 0 var(--space-1); background: transparent; border: 0; color: var(--text-secondary); cursor: pointer; }
.col-row { display: flex; align-items: center; gap: var(--space-1); }
.col-menu label {
flex: 1; display: flex; align-items: center; gap: var(--space-2);
padding: var(--space-1); border-radius: 3px; cursor: pointer; min-width: 0;
}
.body {
flex: 1;
min-height: 0;
padding: var(--space-2);
.col-menu label:hover { background: var(--surface-2); }
.col-menu label.off { opacity: 0.5; }
.col-name { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.rescan { padding: 0 var(--space-2); background: transparent; border: 0; color: var(--text-secondary); cursor: pointer; }
.dim { color: var(--text-secondary); font-size: var(--font-size-sm); }
.col-scan { display: flex; gap: var(--space-1); margin-top: var(--space-2); border-top: 1px solid var(--surface-2); padding-top: var(--space-2); }
.scan-input { flex: 1; min-width: 0; padding: var(--space-1) var(--space-2); background: var(--surface-2); border: var(--panel-border); border-radius: var(--panel-radius); color: var(--text-primary); }
.scan-err { color: var(--status-error, #e0a030); font-size: var(--font-size-sm); margin: var(--space-1) 0 0; }
.search { flex: 1; display: flex; align-items: center; justify-content: center; gap: var(--space-2); }
.search-input {
width: 100%; max-width: 520px;
padding: var(--space-1) var(--space-3);
background: var(--surface-2); border: var(--panel-border); border-radius: var(--panel-radius);
color: var(--text-primary);
}
.search-stats { color: var(--text-secondary); font-size: var(--font-size-sm); white-space: nowrap; }
.pkg { padding: 1px var(--space-2); }
.body { flex: 1; min-height: 0; padding: var(--space-2); }
.err { color: var(--status-error, #e0a030); padding: var(--space-4); }
.stack { display: flex; flex-direction: column; height: 100%; gap: var(--space-2); }
.grow { flex: 1; min-height: 0; }
.bar {
flex-shrink: 0; display: flex; align-items: center; gap: var(--space-2);
justify-content: flex-start; text-align: left; width: 100%;
background: var(--surface-2); border: var(--panel-border); border-radius: var(--panel-radius);
padding: var(--space-1) var(--space-3); color: var(--text-secondary);
}
.bar span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chev { padding: 0 var(--space-2); background: transparent; border: 0; color: var(--text-secondary); cursor: pointer; }
</style>

View File

@@ -0,0 +1,104 @@
<script setup lang="ts">
import { computed, defineAsyncComponent } from 'vue'
import { marked } from 'marked'
import SplitPane from '@framework/components/SplitPane.vue'
import NativeViewer from '@/components/NativeViewer.vue'
import { store, humanBytes } from '@/store'
// Meeting review is a large component (video + transcript + frames) — load it
// lazily only when a meeting is opened (Vue-native embed, no iframe).
const MeetingReview = defineAsyncComponent(() => import('@/components/MeetingReview.vue'))
const isMeeting = computed(() => store.detail?.node.mode === 'meeting')
const d = computed(() => store.detail)
const node = computed(() => d.value?.node)
// Left pane (extracted search text) only exists for extracted heavy formats,
// and not for pptx (whose viewer already shows the per-slide text).
const hasExtracted = computed(() =>
node.value?.mode === 'extracted' && node.value.ext !== 'pptx' && d.value?.content != null)
const extractedHtml = computed(() => (d.value?.content ? marked.parse(d.value.content) : ''))
const metaRows = computed(() => {
const m = d.value?.meta as Record<string, any> | null
if (!m) return []
const out: Array<[string, string]> = []
const push = (k: string, v: unknown) => {
if (v == null || v === '') return
out.push([k, Array.isArray(v) ? v.join(', ') : typeof v === 'object' ? JSON.stringify(v) : String(v)])
}
push('title', m.title); push('author', m.author); push('created', m.created)
push('pages', m.pages); push('slides', m.slides); push('sheets', m.sheets)
push('words', m.word_count); push('extractor', m.extractor)
return out
})
</script>
<template>
<div v-if="!node" class="empty">Select a file to view it.</div>
<div v-else class="detail">
<!-- meetings get no selection header the viewer is the meeting itself -->
<header v-if="node.mode !== 'meeting'" class="head">
<span class="badge" :data-mode="node.mode">{{ node.mode }}</span>
<span class="name">{{ node.path }}</span>
<span class="dim">{{ node.ext }} · {{ humanBytes(node.bytes) }}</span>
</header>
<!-- extracted: search text (left, secondary) | native original (right) -->
<SplitPane v-if="hasExtracted" class="body" :initial-size="1" :min="0.25" :max="3">
<template #first>
<div class="pane">
<div class="pane-label">Extracted text · search index (not the document)</div>
<div class="md" v-html="extractedHtml" />
</div>
</template>
<template #second>
<NativeViewer :node="node" :original-url="d!.originalUrl" :content="d?.content" :text="d?.text" />
</template>
</SplitPane>
<!-- meeting: embed the meetus review (video + transcript + frames) -->
<div v-else-if="isMeeting" class="body single">
<MeetingReview :path="node.path" />
</div>
<!-- link / pptx: single native viewer -->
<div v-else class="body single">
<NativeViewer :node="node" :original-url="d!.originalUrl" :content="d?.content" :text="d?.text" />
</div>
<footer v-if="metaRows.length" class="meta">
<span v-for="[k, v] in metaRows" :key="k" class="chip"><b>{{ k }}</b> {{ v }}</span>
</footer>
</div>
</template>
<style scoped>
.empty { padding: var(--space-4); color: var(--text-secondary); }
.detail { display: flex; flex-direction: column; height: 100%; }
.head {
display: flex; align-items: center; gap: var(--space-3);
padding: var(--space-2) var(--space-3); border-bottom: var(--panel-border); flex-shrink: 0;
}
.head .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; }
.head .dim, .dim { color: var(--text-secondary); font-size: var(--font-size-sm); }
.badge {
font-size: 10px; text-transform: uppercase; padding: 1px 5px; border-radius: 3px;
background: var(--surface-3); color: var(--text-secondary);
}
.badge[data-mode='extracted'] { background: var(--status-live, #2a6); color: #fff; }
.badge[data-mode='meeting'] { background: var(--status-processing, #a60); color: #fff; }
.body { flex: 1; min-height: 0; }
.body.single { overflow: hidden; }
.pane { height: 100%; display: flex; flex-direction: column; overflow: hidden; }
.pane-label {
font-size: 11px; color: var(--text-secondary); padding: var(--space-1) var(--space-3);
border-bottom: 1px solid var(--surface-2); flex-shrink: 0;
}
.md { padding: var(--space-3); overflow: auto; }
.meta { display: flex; flex-wrap: wrap; gap: var(--space-2); padding: var(--space-2) var(--space-3); border-top: var(--panel-border); flex-shrink: 0; }
.chip { font-size: var(--font-size-sm); color: var(--text-secondary); }
.chip b { color: var(--text-primary); }
</style>

View File

@@ -1,85 +0,0 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { store, loadDocs, openDoc, toggleSelect, isSelected } from '@/store'
onMounted(loadDocs)
</script>
<template>
<div class="doc-picker">
<div class="picker-head">
<strong>{{ store.docs.length }}</strong> document{{ store.docs.length === 1 ? '' : 's' }}
</div>
<ul class="doc-list">
<li
v-for="d in store.docs"
:key="d.id"
:class="{ active: d.id === store.currentId }"
@click="openDoc(d.id)"
>
<input
type="checkbox"
:checked="isSelected(d.id)"
title="Add to package"
@click.stop="toggleSelect(d.id)"
/>
<span class="type-badge" :data-type="d.type ?? '?'">{{ d.type ?? '?' }}</span>
<span class="doc-name">{{ d.title || d.name }}</span>
<span v-if="d.warnings" class="warn" :title="`${d.warnings} warning(s)`"></span>
</li>
</ul>
</div>
</template>
<style scoped>
.doc-picker {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.picker-head {
padding: var(--space-2) var(--space-3);
color: var(--text-secondary);
font-size: var(--font-size-sm);
border-bottom: var(--panel-border);
}
.doc-list {
list-style: none;
margin: 0;
padding: 0;
overflow-y: auto;
}
.doc-list li {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
cursor: pointer;
border-bottom: 1px solid var(--surface-2);
}
.doc-list li:hover {
background: var(--surface-2);
}
.doc-list li.active {
background: var(--surface-3);
}
.type-badge {
font-size: 10px;
text-transform: uppercase;
padding: 1px 5px;
border-radius: 3px;
background: var(--surface-3);
color: var(--text-secondary);
flex-shrink: 0;
}
.doc-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.warn {
margin-left: auto;
color: var(--status-error, #e0a030);
}
</style>

View File

@@ -1,76 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import VideoPlayer from '@framework/components/VideoPlayer.vue'
import { store } from '@/store'
// Choose a viewer by file type. Images and video render the original directly;
// everything else renders the textified content.md (monospace for structured
// text, prose for documents).
const kind = computed<'image' | 'video' | 'mono' | 'prose'>(() => {
const t = (store.current?.meta as any)?.type as string | undefined
if (t === 'jpg' || t === 'jpeg' || t === 'png') return 'image'
if (t === 'mp4') return 'video'
if (t === 'json' || t === 'yaml' || t === 'yml' || t === 'csv') return 'mono'
return 'prose'
})
const content = computed(() => store.current?.content ?? '')
</script>
<template>
<div class="viewer">
<div v-if="!store.current" class="empty">Select a document to view its extracted content.</div>
<div v-else-if="kind === 'image'" class="image-wrap">
<img :src="store.current.originalUrl ?? store.current.thumbUrl ?? ''" alt="document" />
</div>
<div v-else-if="kind === 'video'" class="video-wrap">
<VideoPlayer v-if="store.current.originalUrl" :src="store.current.originalUrl" />
<p class="note">
Video preview. The transcript is produced by the meetus pipeline, not doocus.
</p>
</div>
<pre v-else-if="kind === 'mono'" class="mono">{{ content }}</pre>
<div v-else class="prose">{{ content }}</div>
</div>
</template>
<style scoped>
.viewer {
height: 100%;
overflow: auto;
padding: var(--space-4);
}
.empty {
color: var(--text-secondary);
}
.image-wrap img {
max-width: 100%;
border-radius: var(--panel-radius);
border: var(--panel-border);
}
.video-wrap {
max-width: 900px;
}
.note {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.mono {
margin: 0;
white-space: pre-wrap;
overflow-wrap: anywhere;
font-family: var(--font-mono, monospace);
font-size: var(--font-size-sm);
line-height: 1.5;
}
.prose {
white-space: pre-wrap;
overflow-wrap: anywhere;
max-width: 820px;
line-height: 1.6;
}
</style>

View File

@@ -1,36 +1,31 @@
<script setup lang="ts">
import { ref } from 'vue'
import JSZip from 'jszip'
import {
store, TARGETS, target, packageEstimate, humanBytes,
} from '@/store'
import { store, TARGETS, target, packageEstimate, humanBytes } from '@/store'
const building = ref(false)
const error = ref('')
// Build a zip: per selected doc, a folder holding the original source file plus
// the textified content.md and meta.json. Originals are included because the
// permitted services (Gemini web / NotebookLM) receive them directly; content.md
// keeps each doc greppable and small.
// Build a zip of the selected files: each ORIGINAL (what Gemini/NotebookLM
// actually consume) plus, for extracted formats, the content.md search text.
// Folder structure is preserved. Later this can emit Drive links instead of
// bytes once nodes carry a `url`.
async function buildPackage(): Promise<void> {
if (!store.selected.size) return
building.value = true
error.value = ''
try {
const zip = new JSZip()
for (const id of store.selected) {
const summary = store.docs.find((d) => d.id === id)
const res = await fetch(`/api/docs/${encodeURIComponent(id)}`)
if (!res.ok) throw new Error(`fetch ${id} failed`)
const detail = await res.json()
const folder = zip.folder(id.replace(/[\\/]+/g, '__')) ?? zip
folder.file('content.md', detail.content ?? '')
folder.file('meta.json', JSON.stringify(detail.meta ?? {}, null, 2))
if (detail.originalUrl) {
const bin = await fetch(detail.originalUrl)
if (bin.ok) {
const name = (summary?.name) || 'original'
folder.file(name, await bin.blob())
for (const f of store.files.filter((x) => store.selected.has(x.path))) {
const dir = f.path.includes('/') ? f.path.slice(0, f.path.lastIndexOf('/')) : ''
const folder = dir ? (zip.folder(dir) ?? zip) : zip
const bin = await fetch(`/api/original?path=${encodeURIComponent(f.path)}`)
if (bin.ok) folder.file(f.name, await bin.blob())
if (f.mode === 'extracted') {
const detail = await fetch(`/api/detail?path=${encodeURIComponent(f.path)}`)
if (detail.ok) {
const d = await detail.json()
if (d.content) folder.file(`${f.name}.txt`, d.content)
}
}
}
@@ -47,10 +42,6 @@ async function buildPackage(): Promise<void> {
building.value = false
}
}
function clearSelection(): void {
store.selected.clear()
}
</script>
<template>
@@ -63,17 +54,12 @@ function clearSelection(): void {
</label>
<div class="estimate" :class="{ over: packageEstimate.overFiles || packageEstimate.overBytes }">
{{ packageEstimate.docCount }} docs ·
<span :class="{ bad: packageEstimate.overFiles }">
{{ packageEstimate.files }}/{{ target.maxFiles }} files
</span>
·
<span :class="{ bad: packageEstimate.overBytes }">
{{ humanBytes(packageEstimate.bytes) }} / {{ humanBytes(target.maxBytes) }}
</span>
{{ packageEstimate.docCount }} sel ·
<span :class="{ bad: packageEstimate.overFiles }">{{ packageEstimate.files }}/{{ target.maxFiles }} files</span> ·
<span :class="{ bad: packageEstimate.overBytes }">{{ humanBytes(packageEstimate.bytes) }} / {{ humanBytes(target.maxBytes) }}</span>
</div>
<button :disabled="!store.selected.size" @click="clearSelection">Clear</button>
<button :disabled="!store.selected.size" @click="store.selected.clear()">Clear</button>
<button :disabled="!store.selected.size || building" @click="buildPackage">
{{ building ? 'Building' : 'Download package' }}
</button>
@@ -82,29 +68,10 @@ function clearSelection(): void {
</template>
<style scoped>
.export-bar {
display: flex;
align-items: center;
gap: var(--space-3);
font-size: var(--font-size-sm);
}
.target {
display: flex;
align-items: center;
gap: var(--space-1);
color: var(--text-secondary);
}
.estimate {
color: var(--text-secondary);
}
.estimate.over {
color: var(--status-error, #e0a030);
}
.estimate .bad {
color: var(--status-error, #e0a030);
font-weight: 600;
}
.err {
color: var(--status-error, #e0a030);
}
.export-bar { display: flex; align-items: center; gap: var(--space-3); font-size: var(--font-size-sm); }
.target { display: flex; align-items: center; gap: var(--space-1); color: var(--text-secondary); }
.estimate { color: var(--text-secondary); }
.estimate.over { color: var(--status-error, #e0a030); }
.estimate .bad { color: var(--status-error, #e0a030); font-weight: 600; }
.err { color: var(--status-error, #e0a030); }
</style>

View File

@@ -0,0 +1,81 @@
<script setup lang="ts">
import { store, select, toggleSelect, toggleFolder, isUnhandled, type TreeItem } from '@/store'
defineProps<{ items: TreeItem[]; depth?: number }>()
const MODE_BADGE: Record<string, string> = {
extracted: 'ext', meeting: 'mtg', link: 'link',
}
// While searching, the tree is pruned to matches — keep every folder open.
function folderCollapsed(path: string): boolean {
return !store.query && store.collapsed.has(path)
}
</script>
<template>
<ul class="tree" :class="{ root: !depth }">
<li v-for="it in items" :key="it.path">
<!-- folder -->
<template v-if="it.dir">
<div class="row folder" :style="{ paddingLeft: (depth ?? 0) * 14 + 8 + 'px' }" @click="toggleFolder(it.path)">
<span class="twisty">{{ folderCollapsed(it.path) ? '▸' : '▾' }}</span>
<span class="fname">{{ it.name }}</span>
</div>
<FileTree v-show="!folderCollapsed(it.path)" :items="it.children!" :depth="(depth ?? 0) + 1" />
</template>
<!-- file -->
<template v-else>
<div
class="row file"
:class="{ active: it.node?.path === store.selectedPath, meeting: it.node?.mode === 'meeting' }"
:style="{ paddingLeft: (depth ?? 0) * 14 + 8 + 'px' }"
@click="it.node && select(it.node.path)"
>
<input
type="checkbox"
:checked="!!it.node && store.selected.has(it.node.path)"
title="Add to package"
@click.stop="it.node && toggleSelect(it.node.path)"
/>
<span class="badge" :data-mode="it.node?.mode">{{ MODE_BADGE[it.node?.mode ?? ''] ?? it.node?.mode }}</span>
<span class="ext">{{ it.node?.ext }}</span>
<span class="fname" :class="{ unhandled: it.node && isUnhandled(it.node) }">{{ it.node?.title || it.name }}</span>
<span v-if="it.node && isUnhandled(it.node)" class="attn" title="no viewer for this type — needs a handling decision"> needs attention</span>
<span v-else-if="it.node?.warnings?.length" class="warn" title="extraction warnings"></span>
</div>
</template>
</li>
</ul>
</template>
<style scoped>
.tree { list-style: none; margin: 0; padding: 0; }
.tree.root { height: 100%; overflow-y: auto; overflow-x: hidden; }
.row {
display: flex;
align-items: center;
gap: var(--space-2);
padding: 3px var(--space-2);
cursor: pointer;
white-space: nowrap;
font-size: var(--font-size-sm);
}
.row:hover { background: var(--surface-2); }
.row.active { background: var(--surface-3); }
.folder { color: var(--text-secondary); font-weight: 600; }
.twisty { width: 12px; display: inline-block; }
.fname { overflow: hidden; text-overflow: ellipsis; }
.file.meeting .fname { color: var(--text-secondary); }
.badge {
font-size: 9px; text-transform: uppercase; padding: 1px 4px; border-radius: 3px;
background: var(--surface-3); color: var(--text-secondary); flex-shrink: 0;
}
.badge[data-mode='extracted'] { background: var(--status-live, #2a6); color: #fff; }
.badge[data-mode='meeting'] { background: var(--status-processing, #a60); color: #fff; }
.ext { color: var(--text-secondary); font-size: 10px; width: 34px; flex-shrink: 0; }
.fname.unhandled { color: var(--text-secondary); font-style: italic; }
.attn { margin-left: auto; font-size: 10px; color: var(--status-error, #e0a030); flex-shrink: 0; }
.warn { margin-left: auto; color: var(--status-error, #e0a030); }
</style>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
/**
* Embeds the meetus review for a specific meeting — pure composition of meetus's
* own ReviewBody (single source, via the @meetus alias). doocus serves a
* meetus-shaped /api/runs/:id from the meeting's .meetus/ output, so the meetus
* store + components run unchanged here. Read-only (allowEdit=false).
*/
import { watch } from 'vue'
import ReviewBody from '@meetus/components/ReviewBody.vue'
import { useStore } from '@meetus/store'
const props = defineProps<{ path: string }>()
const { openRun, state } = useStore()
// The meeting's rel path IS the run id; openRun hits doocus's /api/runs/:id.
watch(() => props.path, (p) => { if (p) openRun(p) }, { immediate: true })
</script>
<template>
<div class="meeting-review">
<div v-if="state.error" class="note">{{ state.error }}</div>
<div v-else-if="!state.currentRunId" class="note">Loading meeting</div>
<ReviewBody v-else :allow-edit="false" :embedded="true" />
</div>
</template>
<style scoped>
.meeting-review { height: 100%; }
.note { padding: var(--space-4); color: var(--text-secondary); }
</style>

View File

@@ -1,123 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import { store, humanBytes } from '@/store'
// Flatten meta.json into label/value rows, promoting the common fields to the
// top and rendering nested objects/arrays compactly.
const rows = computed(() => {
const m = store.current?.meta as Record<string, any> | undefined
if (!m) return []
const out: Array<{ label: string; value: string }> = []
const push = (label: string, value: unknown) => {
if (value === null || value === undefined || value === '') return
let s: string
if (Array.isArray(value)) s = value.join(', ')
else if (typeof value === 'object') s = JSON.stringify(value)
else s = String(value)
out.push({ label, value: s })
}
const src = (m.source ?? {}) as Record<string, unknown>
push('File', src.name)
push('Type', m.type)
push('Family', m.family)
push('Size', typeof src.bytes === 'number' ? humanBytes(src.bytes) : undefined)
push('Modified', src.modified)
push('Title', m.title)
push('Author', m.author)
push('Created', m.created)
push('Doc modified', m.modified_doc)
push('Pages', m.pages)
push('Slides', m.slides)
push('Sheets', m.sheets)
push('Rows', m.rows)
push('Columns', m.columns)
push('Words', m.word_count)
push('Captured', m.captured)
push('Duration (s)', m.duration_seconds)
push('Delegate', m.delegate)
push('Extractor', m.extractor)
push('sha256', src.sha256)
// Any remaining structured extras not already shown.
if (m.structure) push('Structure', m.structure)
if (m.headings) push('Headings', m.headings)
return out
})
const warnings = computed(() => {
const w = (store.current?.meta as any)?.warnings
return Array.isArray(w) ? w : []
})
</script>
<template>
<div class="meta-panel">
<div v-if="!store.current" class="empty">No document selected.</div>
<template v-else>
<img
v-if="store.current.thumbUrl"
class="thumb"
:src="store.current.thumbUrl"
alt="preview"
/>
<dl class="rows">
<template v-for="r in rows" :key="r.label">
<dt>{{ r.label }}</dt>
<dd>{{ r.value }}</dd>
</template>
</dl>
<div v-if="warnings.length" class="warnings">
<div class="warn-title">Warnings</div>
<ul>
<li v-for="(w, i) in warnings" :key="i">{{ w }}</li>
</ul>
</div>
</template>
</div>
</template>
<style scoped>
.meta-panel {
height: 100%;
overflow-y: auto;
padding: var(--space-3);
}
.empty {
color: var(--text-secondary);
}
.thumb {
width: 100%;
border-radius: var(--panel-radius);
border: var(--panel-border);
margin-bottom: var(--space-3);
}
.rows {
display: grid;
grid-template-columns: max-content 1fr;
gap: var(--space-1) var(--space-3);
margin: 0;
font-size: var(--font-size-sm);
}
.rows dt {
color: var(--text-secondary);
white-space: nowrap;
}
.rows dd {
margin: 0;
overflow-wrap: anywhere;
}
.warnings {
margin-top: var(--space-4);
color: var(--status-error, #e0a030);
font-size: var(--font-size-sm);
}
.warn-title {
font-weight: 600;
margin-bottom: var(--space-1);
}
.warnings ul {
margin: 0;
padding-left: var(--space-4);
}
</style>

View File

@@ -0,0 +1,131 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { marked } from 'marked'
import hljs from 'highlight.js/lib/core'
import yaml from 'highlight.js/lib/languages/yaml'
import json from 'highlight.js/lib/languages/json'
import xml from 'highlight.js/lib/languages/xml'
import ini from 'highlight.js/lib/languages/ini'
import { kindFor, type FileNode } from '@/store'
hljs.registerLanguage('yaml', yaml)
hljs.registerLanguage('json', json)
hljs.registerLanguage('xml', xml)
hljs.registerLanguage('ini', ini)
const props = defineProps<{
node: FileNode
originalUrl: string
content?: string | null // extracted text (pptx fallback render)
text?: string | null // inline verbatim text for text-native originals
}>()
const kind = computed(() => kindFor(props.node.ext))
// Verbatim highlighting for code-ish originals (yaml/json/csv/txt/…).
const HLJS_LANG: Record<string, string> = {
yaml: 'yaml', yml: 'yaml', json: 'json', xml: 'xml', html: 'xml',
ini: 'ini', toml: 'ini', csv: 'plaintext', txt: 'plaintext', log: 'plaintext',
}
const highlighted = computed(() => {
if (kind.value !== 'code') return ''
const src = props.text ?? ''
const lang = HLJS_LANG[props.node.ext] ?? 'plaintext'
try {
return hljs.highlight(src, { language: lang }).value
} catch {
return src.replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]!))
}
})
const mdHtml = computed(() => (props.text ? marked.parse(props.text) : ''))
const pptxHtml = computed(() => (props.content ? marked.parse(props.content) : ''))
// docx / xlsx rendered client-side (lazy).
const rendered = ref('')
const loading = ref(false)
const loadError = ref('')
async function loadRich(): Promise<void> {
rendered.value = ''
loadError.value = ''
if (kind.value !== 'docx' && kind.value !== 'xlsx') return
loading.value = true
try {
const buf = await (await fetch(props.originalUrl)).arrayBuffer()
if (kind.value === 'docx') {
const mammoth = await import('mammoth/mammoth.browser')
rendered.value = (await mammoth.convertToHtml({ arrayBuffer: buf })).value
} else {
const XLSX = await import('xlsx')
const wb = XLSX.read(new Uint8Array(buf), { type: 'array' })
rendered.value = wb.SheetNames.map((n) =>
`<h3>${n}</h3>` + XLSX.utils.sheet_to_html(wb.Sheets[n])).join('\n')
}
} catch (e) {
loadError.value = String(e)
} finally {
loading.value = false
}
}
watch(() => props.node.path, loadRich, { immediate: true })
function openOriginal(): void {
window.open(props.node.url ?? props.originalUrl, '_blank')
}
</script>
<template>
<div class="nv">
<iframe v-if="kind === 'pdf'" class="frame" :src="originalUrl" title="pdf" />
<div v-else-if="kind === 'image'" class="center"><img :src="originalUrl" :alt="node.name" /></div>
<iframe v-else-if="kind === 'html'" class="frame" :src="originalUrl" sandbox="" title="html" />
<div v-else-if="kind === 'markdown'" class="doc md" v-html="mdHtml" />
<pre v-else-if="kind === 'code'" class="hljs code"><code v-html="highlighted" /></pre>
<div v-else-if="kind === 'video'" class="center">
<video class="video" :src="originalUrl" controls preload="metadata" />
</div>
<div v-else-if="kind === 'docx' || kind === 'xlsx'" class="doc rich">
<div v-if="loading" class="dim">Rendering</div>
<div v-else-if="loadError" class="dim">Preview failed: {{ loadError }}</div>
<div v-else v-html="rendered" />
</div>
<div v-else-if="kind === 'pptx'" class="doc md">
<div class="dim label">Slide text (no visual render)</div>
<div v-html="pptxHtml" />
</div>
<!-- unhandled type flagged for a handling decision -->
<div v-else class="center attention">
<div class="warn-icon"></div>
<p>No inline viewer for <b>.{{ node.ext }}</b> needs a handling decision.</p>
<button @click="openOriginal">
{{ node.url ? 'Open in Drive' : 'Open file' }}
</button>
</div>
</div>
</template>
<style scoped>
.nv { height: 100%; overflow: auto; }
.frame { width: 100%; height: 100%; border: 0; }
.center { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-2); padding: var(--space-3); }
.center img { max-width: 100%; }
.video { max-width: 100%; max-height: 78vh; }
.doc { padding: var(--space-4); }
.doc.rich :deep(table) { border-collapse: collapse; }
.doc.rich :deep(td), .doc.rich :deep(th) { border: 1px solid var(--surface-3); padding: 2px 6px; }
.doc.rich :deep(h3) { margin: var(--space-3) 0 var(--space-1); }
.md :deep(img) { max-width: 100%; }
.code { margin: 0; padding: var(--space-3); border-radius: 0; overflow: auto; font-size: var(--font-size-sm); line-height: 1.5; }
.attention .warn-icon { font-size: 28px; color: var(--status-error, #e0a030); }
.dim { color: var(--text-secondary); }
.label { font-size: 11px; margin-bottom: var(--space-2); }
</style>

View File

@@ -5,3 +5,10 @@ declare module '*.vue' {
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
export default component
}
// mammoth ships a browser bundle without bundled types for that entry point.
declare module 'mammoth/mammoth.browser' {
export function convertToHtml(
input: { arrayBuffer: ArrayBuffer },
): Promise<{ value: string; messages: unknown[] }>
}

View File

@@ -1,5 +1,6 @@
import { createApp } from 'vue'
import '@framework/tokens.css'
import 'highlight.js/styles/atom-one-dark.css'
import './styles.css'
import App from './App.vue'

View File

@@ -1,37 +1,68 @@
/**
* Reactive app store for the doocus browser.
* Reactive store for the doocus tree browser.
*
* Reads the local docs-output tree via /api/docs (see server/doocusApi.ts).
* Holds the doc list, the currently open doc (meta + textified content), and the
* selection used by the package builder. Read-only over the pipeline output
* the UI never mutates extracted artifacts.
* Loads the whole-drive tree from /api/tree (index.json), builds a folder
* hierarchy from the flat file list, lazily fetches per-file detail on select,
* and tracks the packaging selection. Read-only over the pipeline output.
*/
import { reactive, computed } from 'vue'
export interface DocSummary {
export interface FileNode {
path: string // composite: "<collectionId>/<relpath>" (API resolution)
collection?: string
root?: string // shared source path (groups docs + meetings of a source)
rel?: string // path within the source (for the merged tree)
name: string
ext: string
family: string
mode: 'extracted' | 'meeting' | 'link'
bytes: number
modified: string
url: string | null
out?: string
words?: number
title?: string
warnings?: string[]
}
export interface CollectionInfo {
id: string
name: string
type: string | null
family: string | null
title: string | null
extractedAt: string | null
wordCount: number
bytes: number
hasThumb: boolean
warnings: number
available: boolean
count: number
root: string | null
only: string
meetings: number
}
export interface DocDetail {
id: string
meta: Record<string, unknown>
content: string
hasThumb: boolean
thumbUrl: string | null
hasOriginal: boolean
originalUrl: string | null
/** A source = one downloaded drive, possibly split into a docs collection and a
* meetings collection that share the same `root`. */
export interface SourceInfo {
root: string
label: string
collectionIds: string[]
docs: number
meetings: number
}
export interface Detail {
node: FileNode
content: string | null // extracted search text (heavy formats), markdown
meta: Record<string, unknown> | null
text: string | null // inline text for text-native originals
mime: string
originalUrl: string
}
/** A folder or file in the display tree. */
export interface TreeItem {
name: string
path: string
dir: boolean
children?: TreeItem[]
node?: FileNode
}
/** Package targets and their (soft) limits. Surfaced, not hard-enforced. */
export interface Target {
key: string
label: string
@@ -45,72 +76,304 @@ export const TARGETS: Target[] = [
]
interface State {
docs: DocSummary[]
outputDir: string
currentId: string | null
current: DocDetail | null
collections: CollectionInfo[]
activeCollections: Set<string>
files: FileNode[]
tree: TreeItem[]
counts: Record<string, number>
selectedPath: string | null
detail: Detail | null
loading: boolean
selected: Set<string>
collapsed: Set<string> // collapsed folder paths (folders default open)
treeCollapsed: boolean // top tree pane collapsed to a bar
detailCollapsed: boolean // bottom viewer collapsed to a bar
targetKey: string
error: string
query: string
matches: Set<string> | null // null = not searching; else the matched paths
searching: boolean
searchStats: { count: number; bytes: number }
scanning: boolean
scanError: string
}
export const store = reactive<State>({
docs: [],
outputDir: '',
currentId: null,
current: null,
collections: [],
activeCollections: new Set<string>(),
files: [],
tree: [],
counts: {},
selectedPath: null,
detail: null,
loading: false,
selected: new Set<string>(),
collapsed: new Set<string>(),
treeCollapsed: false,
detailCollapsed: false,
targetKey: TARGETS[0].key,
error: '',
query: '',
matches: null,
searching: false,
searchStats: { count: 0, bytes: 0 },
scanning: false,
scanError: '',
})
export async function loadDocs(): Promise<void> {
const res = await fetch('/api/docs')
const data = await res.json()
store.docs = data.docs ?? []
store.outputDir = data.outputDir ?? ''
if (!store.currentId && store.docs.length) {
await openDoc(store.docs[0].id)
let searchTimer: ReturnType<typeof setTimeout> | null = null
/** Search the cached text (transcripts, extracted docs, raw files) and prune the
* tree to matches. Debounced; reliability over speed (server reads full text). */
export function runSearch(q: string): void {
store.query = q
if (searchTimer) clearTimeout(searchTimer)
if (!q.trim()) {
store.matches = null
store.searching = false
store.searchStats = { count: 0, bytes: 0 }
return
}
store.searching = true
searchTimer = setTimeout(async () => {
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}&collections=${encodeURIComponent(activeCsv())}`)
if (!res.ok) return
const d = await res.json()
if (store.query !== q) return // a newer query superseded this one
store.matches = new Set<string>(d.matches ?? [])
store.searchStats = { count: d.count ?? 0, bytes: d.bytes ?? 0 }
} finally {
if (store.query === q) store.searching = false
}
}, 250)
}
/** Tree pruned to search matches (folders auto-expand while searching). */
export const visibleTree = computed(() =>
store.matches ? buildTree(store.files.filter((f) => store.matches!.has(f.path))) : store.tree)
/** Put the current match set into the package selection (one package for now). */
export function selectMatches(): void {
if (store.matches) store.selected = new Set(store.matches)
}
export function toggleFolder(path: string): void {
if (store.collapsed.has(path)) store.collapsed.delete(path)
else store.collapsed.add(path)
}
/** Unique display label per source root (top-level tree group). */
function sourceLabels(files: FileNode[]): Map<string, string> {
const roots = [...new Set(files.map((f) => f.root).filter(Boolean))] as string[]
const labels = new Map<string, string>()
const seen = new Set<string>()
for (const r of roots.sort()) {
const base = r.split('/').filter(Boolean).pop() || r
let label = base, n = 1
while (seen.has(label)) { n++; label = `${base}-${n}` }
seen.add(label)
labels.set(r, label)
}
return labels
}
/** Build the tree grouped by source root — so a source's docs + meetings
* interleave under one folder — while each file keeps its composite `node.path`
* for API calls. Folder items are keyed by their display path. */
function buildTree(files: FileNode[]): TreeItem[] {
const labels = sourceLabels(files)
const root: TreeItem = { name: '', path: '', dir: true, children: [] }
for (const f of files) {
const src = (f.root && labels.get(f.root)) || f.collection || 'source'
const display = `${src}/${f.rel ?? f.path}`
const parts = display.split('/')
let cur = root
for (let i = 0; i < parts.length - 1; i++) {
const seg = parts[i]
const p = parts.slice(0, i + 1).join('/')
let next = cur.children!.find((c) => c.dir && c.name === seg)
if (!next) {
next = { name: seg, path: p, dir: true, children: [] }
cur.children!.push(next)
}
cur = next
}
cur.children!.push({ name: f.name, path: display, dir: false, node: f })
}
const sort = (items: TreeItem[]): TreeItem[] => {
items.sort((a, b) =>
a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1)
for (const it of items) if (it.dir) sort(it.children!)
return items
}
return sort(root.children!)
}
function activeCsv(): string {
return [...store.activeCollections].join(',')
}
/** Load available collections; default every available one to active. */
export async function loadCollections(): Promise<void> {
try {
const res = await fetch('/api/collections')
const data = await res.json()
store.collections = data.collections ?? []
// Default: all available collections on.
store.activeCollections = new Set(store.collections.filter((c) => c.available).map((c) => c.id))
} catch (e) {
store.error = String(e)
}
}
export async function openDoc(id: string): Promise<void> {
/** Re-discover sources (picks up newly-scanned collections) without a page
* reload; preserves current toggles and turns on any brand-new source. */
export async function refreshSources(): Promise<void> {
const prevActive = new Set(store.activeCollections)
const known = new Set(store.collections.map((c) => c.id))
await loadCollections() // sets active = all available
store.activeCollections = new Set(
store.collections
.filter((c) => c.available && (prevActive.has(c.id) || !known.has(c.id)))
.map((c) => c.id),
)
await loadTree()
if (store.query) runSearch(store.query)
}
export async function loadTree(): Promise<void> {
store.loading = true
store.error = ''
try {
const res = await fetch(`/api/docs/${encodeURIComponent(id)}`)
if (!res.ok) throw new Error(`open failed: ${res.status}`)
store.current = await res.json()
store.currentId = id
if (!store.collections.length) await loadCollections()
const res = await fetch(`/api/tree?collections=${encodeURIComponent(activeCsv())}`)
if (!res.ok) { store.error = (await res.json()).error ?? `HTTP ${res.status}`; return }
const index = await res.json()
store.files = index.files
store.counts = index.counts
store.tree = buildTree(index.files)
} catch (e) {
store.error = String(e)
} finally {
store.loading = false
}
}
export function toggleSelect(id: string): void {
if (store.selected.has(id)) store.selected.delete(id)
else store.selected.add(id)
/** Collections grouped by shared root — the selector unit. */
export const sources = computed<SourceInfo[]>(() => {
const byRoot = new Map<string, SourceInfo>()
const seen = new Set<string>()
for (const c of store.collections) {
if (!c.root) continue
let s = byRoot.get(c.root)
if (!s) {
const base = c.root.split('/').filter(Boolean).pop() || c.root
let label = base, n = 1
while (seen.has(label)) { n++; label = `${base}-${n}` }
seen.add(label)
s = { root: c.root, label, collectionIds: [], docs: 0, meetings: 0 }
byRoot.set(c.root, s)
}
s.collectionIds.push(c.id)
s.docs += c.count - c.meetings
s.meetings += c.meetings
}
return [...byRoot.values()]
})
export function sourceActive(root: string): boolean {
const s = sources.value.find((x) => x.root === root)
return !!s && s.collectionIds.some((id) => store.activeCollections.has(id))
}
export function isSelected(id: string): boolean {
return store.selected.has(id)
/** Toggle a whole source (its docs + meetings collections) and re-merge. */
export async function toggleSource(root: string): Promise<void> {
const s = sources.value.find((x) => x.root === root)
if (!s) return
const allOn = s.collectionIds.every((id) => store.activeCollections.has(id))
for (const id of s.collectionIds) {
if (allOn) store.activeCollections.delete(id)
else store.activeCollections.add(id)
}
await loadTree()
if (store.query) runSearch(store.query)
}
/** Scan a source folder (runs process_tree.py server-side) → new collection. */
export async function scanFolder(source: string): Promise<void> {
if (!source.trim()) return
store.scanning = true
store.scanError = ''
try {
const res = await fetch('/api/scan', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source: source.trim() }),
})
const data = await res.json()
if (!res.ok) { store.scanError = data.error ?? `HTTP ${res.status}`; return }
await loadCollections()
if (data.id) store.activeCollections.add(data.id) // show the new source
await loadTree()
} catch (e) {
store.scanError = String(e)
} finally {
store.scanning = false
}
}
/** Re-run the scan for an existing collection (its recorded source). */
export async function rescanCollection(id: string): Promise<void> {
store.scanning = true
store.scanError = ''
try {
const res = await fetch('/api/rescan', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id }),
})
const data = await res.json()
if (!res.ok) { store.scanError = data.error ?? `HTTP ${res.status}`; return }
await loadCollections()
await loadTree()
if (store.query) runSearch(store.query)
} catch (e) {
store.scanError = String(e)
} finally {
store.scanning = false
}
}
export async function select(path: string): Promise<void> {
store.selectedPath = path
store.detailCollapsed = false // opening a file expands the viewer
store.detail = null
const res = await fetch(`/api/detail?path=${encodeURIComponent(path)}`)
if (res.ok) store.detail = await res.json()
}
export function toggleSelect(path: string): void {
if (store.selected.has(path)) store.selected.delete(path)
else store.selected.add(path)
}
export const target = computed(() => TARGETS.find((t) => t.key === store.targetKey) ?? TARGETS[0])
/** Rough package estimate: each doc contributes original + content.md + meta.json. */
/** Package estimate: one original per selected file (+ a small sidecar for extracted). */
export const packageEstimate = computed(() => {
const chosen = store.docs.filter((d) => store.selected.has(d.id))
const files = chosen.length * 3 // original + content.md + meta.json
// originals dominate size; content/meta are small — approximate with wordCount.
const bytes = chosen.reduce((sum, d) => sum + d.bytes + d.wordCount * 6 + 512, 0)
const t = target.value
return {
docCount: chosen.length,
files,
bytes,
overFiles: files > t.maxFiles,
overBytes: bytes > t.maxBytes,
const chosen = store.files.filter((f) => store.selected.has(f.path))
let files = 0
let bytes = 0
for (const f of chosen) {
files += 1 // the original
bytes += f.bytes
if (f.mode === 'extracted') { files += 1; bytes += (f.words ?? 0) * 6 + 512 } // content.md
}
const t = target.value
return { docCount: chosen.length, files, bytes, overFiles: files > t.maxFiles, overBytes: bytes > t.maxBytes }
})
export function humanBytes(n: number): string {
@@ -118,3 +381,26 @@ export function humanBytes(n: number): string {
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
return `${(n / 1024 / 1024).toFixed(1)} MB`
}
export type ViewerKind =
| 'pdf' | 'image' | 'html' | 'markdown' | 'code' | 'docx' | 'xlsx' | 'pptx' | 'video' | 'unhandled'
/** Single source of truth for how a file is viewed — shared by the tree (to flag
* unhandled types) and the viewer. 'unhandled' = no inline view; default action
* is to open the original file (or the Drive link once we have it). */
export function kindFor(ext: string): ViewerKind {
if (ext === 'pdf') return 'pdf'
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) return 'image'
if (['html', 'htm'].includes(ext)) return 'html'
if (['md', 'markdown'].includes(ext)) return 'markdown'
if (['txt', 'json', 'yaml', 'yml', 'csv', 'log', 'ini', 'xml', 'toml'].includes(ext)) return 'code'
if (ext === 'docx') return 'docx'
if (ext === 'xlsx') return 'xlsx'
if (ext === 'pptx') return 'pptx'
if (['mp4', 'mkv', 'mov', 'm4v', 'webm', 'avi', 'wmv'].includes(ext)) return 'video'
return 'unhandled'
}
export function isUnhandled(node: FileNode): boolean {
return node.mode !== 'meeting' && kindFor(node.ext) === 'unhandled'
}

View File

@@ -14,6 +14,7 @@
"baseUrl": ".",
"paths": {
"@framework/*": ["../framework/src/*"],
"@meetus/*": ["../meetus-app/src/*"],
"@/*": ["src/*"],
"vue": ["node_modules/vue"]
}

View File

@@ -3,26 +3,47 @@ import vue from '@vitejs/plugin-vue'
import { fileURLToPath } from 'node:url'
import { doocusApi } from './server/doocusApi'
// Output directory produced by the doocus pipeline (process_doc.py /
// ctrl/batch_docs.sh). Override with DOOCUS_OUTPUT.
const outputDir = process.env.DOOCUS_OUTPUT
?? fileURLToPath(new URL('../../docs-output', import.meta.url))
// Explicit output dirs (env). DOOCUS_OUTPUTS is comma-separated; DOOCUS_OUTPUT
// (single) is honored too. Defaults to the repo's docs-output for back-compat.
const outputEnv = process.env.DOOCUS_OUTPUTS ?? process.env.DOOCUS_OUTPUT
const outputDirs = (outputEnv
? outputEnv.split(',').map((s) => s.trim()).filter(Boolean)
: [fileURLToPath(new URL('../../docs-output', import.meta.url))])
// Managed collection roots (gitignored), discovered automatically:
// doocus-data/<source>/ → document collections (UI "Scan" writes here)
// meetus-data/<source>/ → meeting collections (from the meetus batch)
// Collections that share the same index.json `root` are the same source and are
// grouped together in the UI. Override with DOOCUS_DATA (comma-separated).
const outputsRoots = process.env.DOOCUS_DATA
? process.env.DOOCUS_DATA.split(',').map((s) => s.trim()).filter(Boolean)
: [
fileURLToPath(new URL('../../doocus-data', import.meta.url)),
fileURLToPath(new URL('../../meetus-data', import.meta.url)),
]
// Repo root — where process_tree.py lives (run via `uv run` for the scan).
const repoRoot = fileURLToPath(new URL('../../', import.meta.url))
export default defineConfig({
plugins: [vue(), doocusApi({ outputDir })],
plugins: [vue(), doocusApi({ outputDirs, outputsRoots, repoRoot })],
resolve: {
alias: {
'@framework': fileURLToPath(new URL('../framework/src', import.meta.url)),
// Compose meetus's own review components (single source; portable imports).
'@meetus': fileURLToPath(new URL('../meetus-app/src', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
fs: {
// Allow the dev server to read the framework source and the docs-output tree.
// Allow the dev server to read the framework + meetus-app source and every output tree.
allow: [
fileURLToPath(new URL('.', import.meta.url)),
fileURLToPath(new URL('../framework', import.meta.url)),
outputDir,
fileURLToPath(new URL('../meetus-app', import.meta.url)),
...outputDirs,
...outputsRoots,
],
},
},

View File

@@ -45,7 +45,8 @@ const VIDEO_MIME: Record<string, string> = {
}
export function meetusApi(opts: Options): Plugin {
const outputDir = path.resolve(opts.outputDir)
// Mutable so the UI can re-point the scan at another folder at runtime.
let outputDir = path.resolve(opts.outputDir)
const videoDir = opts.videoDir ? path.resolve(opts.videoDir) : null
/** Locate a run's source video. The manifest records an absolute path from
@@ -139,6 +140,22 @@ export function meetusApi(opts: Options): Plugin {
parts: string[],
logger: { warn: (m: string) => void },
): Promise<boolean> {
// GET/PUT the folder scanned (recursively) for meeting runs.
if (parts[0] === 'output') {
if (req.method === 'GET') { sendJson(res, 200, { dir: outputDir }); return true }
if (req.method === 'PUT') {
let body: { dir?: string }
try { body = JSON.parse((await readBody(req)) || '{}') } catch { sendJson(res, 400, { error: 'invalid JSON' }); return true }
const dir = (body.dir ?? '').trim()
try {
if (!dir || !fs.statSync(dir).isDirectory()) throw new Error('not a directory')
} catch { sendJson(res, 400, { error: `not a directory: ${dir}` }); return true }
outputDir = path.resolve(dir)
sendJson(res, 200, { dir: outputDir })
return true
}
}
if (parts[0] !== 'runs') return false
// GET /api/runs

View File

@@ -1,14 +1,7 @@
<script setup lang="ts">
import SplitPane from '@framework/components/SplitPane.vue'
import Panel from '@framework/components/Panel.vue'
import VideoPlayer from '@framework/components/VideoPlayer.vue'
import RunPicker from '@/components/RunPicker.vue'
import TranscriptEditor from '@/components/TranscriptEditor.vue'
import FrameSelector from '@/components/FrameSelector.vue'
import ExportBar from '@/components/ExportBar.vue'
import { useStore } from '@/store'
const { state } = useStore()
import ReviewBody from '@/components/ReviewBody.vue'
</script>
<template>
@@ -20,34 +13,7 @@ const { state } = useStore()
</header>
<div class="body">
<SplitPane direction="horizontal" :initialSize="1.4" sizeMode="ratio" :min="0.4" :max="4">
<template #first>
<SplitPane direction="vertical" :initialSize="1.3" sizeMode="ratio" :min="0.3" :max="4">
<template #first>
<Panel title="Video" :status="state.playing ? 'live' : 'idle'">
<VideoPlayer
v-if="state.videoUrl"
:src="state.videoUrl"
v-model:currentTime="state.currentTime"
v-model:playing="state.playing"
@duration="state.duration = $event"
/>
<div v-else class="placeholder">
{{ state.currentRunId
? 'Source video not available at the path recorded in manifest.json.'
: 'Select a meeting to begin.' }}
</div>
</Panel>
</template>
<template #second>
<FrameSelector />
</template>
</SplitPane>
</template>
<template #second>
<TranscriptEditor />
</template>
</SplitPane>
<ReviewBody :allow-edit="true" />
</div>
</div>
</template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, watch, nextTick, computed } from 'vue'
import Panel from '@framework/components/Panel.vue'
import { useStore } from '@/store'
import { useStore } from '../store'
const { state, activeFrameIndex, selectedFrames, seek, setSelectAll } = useStore()

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
/**
* The meetus review — pure composition of video + frames + transcript over the
* shared store. Uses only relative + @framework imports so it is portable: the
* doocus app embeds this same component (pointed at its own meetus-shaped API).
* Config is by prop; all logic lives in the store and child components.
*/
import SplitPane from '@framework/components/SplitPane.vue'
import Panel from '@framework/components/Panel.vue'
import VideoPlayer from '@framework/components/VideoPlayer.vue'
import TranscriptEditor from './TranscriptEditor.vue'
import FrameSelector from './FrameSelector.vue'
import { useStore } from '../store'
withDefaults(defineProps<{ allowEdit?: boolean; embedded?: boolean }>(), {
allowEdit: true, embedded: false,
})
const { state } = useStore()
</script>
<template>
<div class="review-body">
<SplitPane direction="horizontal" :initialSize="1.4" sizeMode="ratio" :min="0.4" :max="4">
<template #first>
<SplitPane direction="vertical" :initialSize="1.3" sizeMode="ratio" :min="0.3" :max="4">
<template #first>
<Panel title="Video" :status="state.playing ? 'live' : 'idle'">
<VideoPlayer
v-if="state.videoUrl"
:src="state.videoUrl"
v-model:currentTime="state.currentTime"
v-model:playing="state.playing"
@duration="state.duration = $event"
/>
<div v-else class="placeholder">
{{ state.currentRunId
? 'Source video not available at the path recorded in manifest.json.'
: 'Select a meeting to begin.' }}
</div>
</Panel>
</template>
<template #second><FrameSelector /></template>
</SplitPane>
</template>
<template #second><TranscriptEditor :allow-edit="allowEdit" /></template>
</SplitPane>
</div>
</template>
<style scoped>
.review-body { height: 100%; }
.placeholder {
display: flex; align-items: center; justify-content: center; height: 100%;
padding: var(--space-6); text-align: center; color: var(--text-secondary);
}
</style>

View File

@@ -1,15 +1,23 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { onMounted, ref } from 'vue'
import { useStore } from '@/store'
const { state, loadRuns, openRun } = useStore()
const { state, loadRuns, openRun, setOutputDir } = useStore()
onMounted(loadRuns)
const showFolder = ref(false)
const folder = ref('')
function onChange(e: Event) {
const id = (e.target as HTMLSelectElement).value
if (id) openRun(id)
}
async function scanFolder() {
await setOutputDir(folder.value)
if (!state.error) showFolder.value = false
}
</script>
<template>
@@ -21,11 +29,21 @@ function onChange(e: Event) {
{{ r.name }} · {{ r.frameCount }} frames{{ r.hasVideo ? '' : ' · no video' }}
</option>
</select>
<button class="folder-btn" :title="`Scan folder — ${state.outputDir ?? ''}`" @click="showFolder = !showFolder">📁</button>
<span v-if="state.loading" class="hint">loading</span>
<span v-else-if="state.error" class="hint err">{{ state.error }}</span>
<span v-else-if="!state.runs.length" class="hint" :title="state.outputDir ?? ''">
no runs found in {{ state.outputDir ?? '(unknown)' }}
</span>
<div v-if="showFolder" class="folder-pop">
<div class="pop-title">Scan folder (recursive) for meetings</div>
<div class="pop-row">
<input v-model="folder" :placeholder="state.outputDir ?? '/path/to/meetings'" @keyup.enter="scanFolder" />
<button :disabled="state.loading || !folder.trim()" @click="scanFolder">Scan</button>
</div>
<div class="pop-cur">current: {{ state.outputDir ?? '(unset)' }}</div>
</div>
</div>
</template>
@@ -34,7 +52,18 @@ function onChange(e: Event) {
display: flex;
align-items: center;
gap: var(--space-2);
position: relative;
}
.folder-btn { padding: var(--space-1) var(--space-2); line-height: 1; }
.folder-pop {
position: absolute; top: 110%; left: 0; z-index: 20; min-width: 340px;
background: var(--surface-1); border: var(--panel-border); border-radius: var(--panel-radius);
padding: var(--space-2); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
}
.pop-title { font-size: 11px; color: var(--text-secondary); margin-bottom: var(--space-1); }
.pop-row { display: flex; gap: var(--space-1); }
.pop-row input { flex: 1; min-width: 0; padding: var(--space-1) var(--space-2); }
.pop-cur { font-size: var(--font-size-sm); color: var(--text-secondary); margin-top: var(--space-1); overflow: hidden; text-overflow: ellipsis; }
.lbl {
color: var(--text-secondary);
font-size: var(--font-size-sm);

View File

@@ -1,13 +1,31 @@
<script setup lang="ts">
import { ref, watch, nextTick, computed, onMounted } from 'vue'
import Panel from '@framework/components/Panel.vue'
import { useStore } from '@/store'
import { useStore } from '../store'
const { state, activeSegmentIndex, seek } = useStore()
const props = withDefaults(defineProps<{ allowEdit?: boolean }>(), { allowEdit: true })
// View preferences (persisted).
const { state, activeSegmentIndex, seek, setSelectMode, setSegmentIncluded } = useStore()
// Turn-level include (a turn = a speaker's continuous run of segments).
function turnIncluded(items: Array<{ i: number }>): boolean {
return items.length > 0 && items.every((it) => state.segments[it.i]?.included)
}
function toggleTurn(items: Array<{ i: number }>): void {
const val = !turnIncluded(items)
for (const it of items) setSegmentIncluded(it.i, val)
}
function toggleSelect(): void {
const on = !state.selectMode
setSelectMode(on)
if (on) viewMode.value = 'read' // selection lives in the read view
}
// View preferences (persisted). When edit is disallowed (e.g. embedded review),
// the transcript is read-only regardless of the saved preference.
type ViewMode = 'edit' | 'read'
const viewMode = ref<ViewMode>((localStorage.getItem('meetus.viewMode') as ViewMode) || 'edit')
const effectiveMode = computed<ViewMode>(() => (props.allowEdit ? viewMode.value : 'read'))
const compact = ref(localStorage.getItem('meetus.compact') === '1')
const showTimes = ref(localStorage.getItem('meetus.readTimes') !== '0')
watch(viewMode, (v) => localStorage.setItem('meetus.viewMode', v))
@@ -83,17 +101,18 @@ watch(activeSegmentIndex, async (i) => {
<template>
<Panel title="Transcript">
<template #actions>
<div class="seg-control">
<button :class="{ on: viewMode === 'edit' }" @click="viewMode = 'edit'">Edit</button>
<button :class="{ on: viewMode === 'read' }" @click="viewMode = 'read'">Read</button>
<div v-if="allowEdit" class="seg-control">
<button :class="{ on: effectiveMode === 'edit' }" @click="viewMode = 'edit'">Edit</button>
<button :class="{ on: effectiveMode === 'read' }" @click="viewMode = 'read'">Read</button>
</div>
<button v-if="viewMode === 'edit'" :class="{ on: compact }" @click="compact = !compact">Compact</button>
<button v-if="allowEdit && effectiveMode === 'edit'" :class="{ on: compact }" @click="compact = !compact">Compact</button>
<button v-else :class="{ on: showTimes }" @click="showTimes = !showTimes">Times</button>
<button :class="{ on: state.selectMode }" title="Pick segments (+ their frames) for the package" @click="toggleSelect">Select</button>
<span class="count">{{ state.segments.length }} segments</span>
</template>
<!-- EDIT: per-segment blocks (comfortable or compact density) -->
<div v-if="viewMode === 'edit'" class="seg-list" :class="{ compact }">
<div v-if="effectiveMode === 'edit'" class="seg-list" :class="{ compact }">
<p v-if="!state.segments.length" class="empty">No transcript for this run.</p>
<div
v-for="(seg, i) in state.segments"
@@ -123,6 +142,14 @@ watch(activeSegmentIndex, async (i) => {
<p v-if="!state.segments.length" class="empty">No transcript for this run.</p>
<div v-for="(turn, ti) in readingTurns" :key="ti" class="turn">
<div class="turn-head">
<input
v-if="state.selectMode"
type="checkbox"
class="turn-pick"
:checked="turnIncluded(turn.items)"
title="Include this part (+ its frames) in the package"
@change="toggleTurn(turn.items)"
/>
<button class="ts" title="Seek here" @click="seek(turn.start)">{{ fmt(turn.start) }}</button>
<span class="speaker-lbl">{{ turn.speaker }}</span>
</div>
@@ -137,9 +164,9 @@ watch(activeSegmentIndex, async (i) => {
><span
:ref="(el) => setSeg(it.i, el as Element | null)"
class="rseg"
:class="{ active: it.i === activeSegmentIndex }"
:class="{ active: it.i === activeSegmentIndex, included: state.selectMode && state.segments[it.i]?.included }"
@click="seek(it.start)"
>{{ it.text }} </span>
>{{ it.text.trim() + ' ' }}</span>
</template>
</p>
</div>
@@ -270,6 +297,10 @@ watch(activeSegmentIndex, async (i) => {
background: var(--surface-2);
box-shadow: 0 0 0 1px var(--status-processing);
}
.rseg.included {
background: color-mix(in srgb, var(--status-live, #2a6) 22%, transparent);
}
.turn-pick { margin-right: var(--space-1); }
.inline-ts {
font-family: var(--font-mono);
font-size: 10px;

Binary file not shown.

963
uv.lock generated Normal file
View File

@@ -0,0 +1,963 @@
version = 1
revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.12'",
"python_full_version == '3.11.*'",
"python_full_version < '3.11'",
]
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "anyio"
version = "4.14.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" },
]
[[package]]
name = "beautifulsoup4"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "soupsieve" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" },
]
[[package]]
name = "certifi"
version = "2026.6.17"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
]
[[package]]
name = "et-xmlfile"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
]
[[package]]
name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
name = "ffmpeg-python"
version = "0.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "future" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dd/5e/d5f9105d59c1325759d838af4e973695081fbbc97182baf73afc78dec266/ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", size = 21543, upload-time = "2019-07-06T00:19:08.989Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/0c/56be52741f75bad4dc6555991fabd2e07b432d333da82c11ad701123888a/ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5", size = 25024, upload-time = "2019-07-06T00:19:07.215Z" },
]
[[package]]
name = "future"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "idna"
version = "3.18"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
]
[[package]]
name = "lxml"
version = "6.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/da/dbe4dfc01ac226fb0504fad035f4d69f3202f3502e20e68537631daddd96/lxml-6.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60", size = 8541124, upload-time = "2026-05-18T19:17:11.589Z" },
{ url = "https://files.pythonhosted.org/packages/78/20/f7095ed9fc2c025f9cfe71cc6ec9f1feb05624edc1812423b5f1aecf3d4b/lxml-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d", size = 4602783, upload-time = "2026-05-18T19:17:20.888Z" },
{ url = "https://files.pythonhosted.org/packages/4a/a4/65c63ca98bd129f6cff7b8c2fa48953ab058cc6005b541354e7dd54d8000/lxml-6.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea", size = 5002687, upload-time = "2026-05-18T19:17:01.738Z" },
{ url = "https://files.pythonhosted.org/packages/96/1d/ab7a5c4b5a394d98a94e2d0fc67bab8297597426770dd4978370fbdaf531/lxml-6.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074", size = 5155099, upload-time = "2026-05-18T19:17:05.159Z" },
{ url = "https://files.pythonhosted.org/packages/d0/b1/07603bfeeb891a2596d5c2a68f7d2f70f7d11c841ebe391412c69c2857b0/lxml-6.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30", size = 5057225, upload-time = "2026-05-18T19:17:08.117Z" },
{ url = "https://files.pythonhosted.org/packages/7a/16/cb391ee4b90186fa16d9ebcbe3ea96c71b8da3b0686386c8dcbcc3c67d44/lxml-6.1.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315", size = 5287643, upload-time = "2026-05-18T19:17:11.507Z" },
{ url = "https://files.pythonhosted.org/packages/eb/d6/b619717f918fd76747448fdbaee0e769edbc70e659b5b5d0112b7020b7a3/lxml-6.1.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1", size = 5412445, upload-time = "2026-05-18T19:17:22.182Z" },
{ url = "https://files.pythonhosted.org/packages/c6/80/12bc5390ac0a3edeb579d9535e5049a5dda663438728e179d52fb319c33a/lxml-6.1.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206", size = 4770864, upload-time = "2026-05-18T19:17:26.851Z" },
{ url = "https://files.pythonhosted.org/packages/0b/59/6500c09da3137f54f020e908d81cfc5ee3e8888e908fd380207afad7c2e6/lxml-6.1.1-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067", size = 5359594, upload-time = "2026-05-18T19:17:32.527Z" },
{ url = "https://files.pythonhosted.org/packages/f2/9b/f64b4cc6b7ebcf75d95af3cde934d254b5f2f10d4163928d838d86b6eb48/lxml-6.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a", size = 5107713, upload-time = "2026-05-18T19:17:04.402Z" },
{ url = "https://files.pythonhosted.org/packages/16/19/c7388ad5d3a72315d2832dc1458cbf4f2af7f2b990b606ff4876efd04511/lxml-6.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa", size = 4803973, upload-time = "2026-05-18T19:17:06.545Z" },
{ url = "https://files.pythonhosted.org/packages/3f/22/76197f0bbf165f0b9e75be59be4997e5259cde973f12f098c1b54c7f5d60/lxml-6.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383", size = 5349925, upload-time = "2026-05-18T19:17:09.743Z" },
{ url = "https://files.pythonhosted.org/packages/24/52/d2a0cfeccb9bcdc47c7ee05cdae5d69b48c9acf20997790a6338bb0d0b3b/lxml-6.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1", size = 5309825, upload-time = "2026-05-18T19:17:13.831Z" },
{ url = "https://files.pythonhosted.org/packages/19/4a/b30944266776c2f49749ef2445aa7e78898194134b80ad776386f61b56ae/lxml-6.1.1-cp310-cp310-win32.whl", hash = "sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a", size = 3598402, upload-time = "2026-05-18T19:17:08.21Z" },
{ url = "https://files.pythonhosted.org/packages/9e/97/33691c66a4d7ec1a5a98e7c909a5b83ee45c7f7ba4cf92b1c4cf26e98079/lxml-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5", size = 4021295, upload-time = "2026-05-18T19:17:28.638Z" },
{ url = "https://files.pythonhosted.org/packages/d0/5f/26a4dd0e12b9456ff7b12a21af5b491eb6629680d1edd73f4140fd386bcf/lxml-6.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485", size = 3667717, upload-time = "2026-05-19T19:22:44.474Z" },
{ url = "https://files.pythonhosted.org/packages/62/b0/83f481780d1548750b8ce2ec824073deef2f452d9cd1a6faff8507e3d16d/lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2", size = 8526461, upload-time = "2026-05-18T19:17:25.862Z" },
{ url = "https://files.pythonhosted.org/packages/b9/d5/30fa0f808002c7329397bfbb24e306789c0b29f04aa5842c07b174b4216f/lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d", size = 4595375, upload-time = "2026-05-18T19:17:34.555Z" },
{ url = "https://files.pythonhosted.org/packages/4f/d2/edb71cf0e561581a7c5eb2626244320eb04e9f8ce6d563184fd668b45073/lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510", size = 4923654, upload-time = "2026-05-18T19:17:42.917Z" },
{ url = "https://files.pythonhosted.org/packages/4c/77/1bc7eeb0de4577d783fb625aa092cc9357883bba35845a3666bf1259f3dc/lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a", size = 5067921, upload-time = "2026-05-18T19:17:49.175Z" },
{ url = "https://files.pythonhosted.org/packages/1b/3c/c0690d74bd2bc17bc03b5b0d093569ead597dd0bfa088bf99eef8c24e19c/lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d", size = 5002456, upload-time = "2026-05-18T19:17:59.715Z" },
{ url = "https://files.pythonhosted.org/packages/66/8d/d1b3271af0c0f1e27e8472a849e4d2c65bc7766884b9ad2da9e76e145c88/lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8", size = 5202776, upload-time = "2026-05-18T19:18:08.924Z" },
{ url = "https://files.pythonhosted.org/packages/7a/45/689824ffb237fd10125ad273f32b28ff04dc6203c2822c85ff65a93df65e/lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009", size = 5329945, upload-time = "2026-05-18T19:18:13.673Z" },
{ url = "https://files.pythonhosted.org/packages/5d/c0/ef73af53767e958fd87d437c170f272e2f6e6c0f854939f133a895f1e711/lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6", size = 4659237, upload-time = "2026-05-18T19:18:18.657Z" },
{ url = "https://files.pythonhosted.org/packages/a0/5e/e1158e40397585e91cb0472374a1f63d0926a1ddeaa92f13d1a1ffe306d5/lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8", size = 5265904, upload-time = "2026-05-18T19:18:24.883Z" },
{ url = "https://files.pythonhosted.org/packages/a0/16/8687e5d1400ed1c0bc41dace232ebb7553952b618ea1f2e5fb6e2cfbbe23/lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83", size = 5045225, upload-time = "2026-05-18T19:17:20.073Z" },
{ url = "https://files.pythonhosted.org/packages/ca/18/d877bd1ae2e5ffdfd4836565aba350db31feb2f2656d6ce70316ed66a05e/lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6", size = 4712721, upload-time = "2026-05-18T19:17:40.512Z" },
{ url = "https://files.pythonhosted.org/packages/44/4d/1f44fd1d770b10dacbf6b5c6e520f4d6e0708744930f719dc04e67cab981/lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c", size = 5252549, upload-time = "2026-05-18T19:17:51.236Z" },
{ url = "https://files.pythonhosted.org/packages/64/5d/1d66b84f850089254c230ef6ea6b267a5a54e2e179a5d960036a05d501d7/lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08", size = 5226877, upload-time = "2026-05-18T19:18:00.875Z" },
{ url = "https://files.pythonhosted.org/packages/ad/00/84c4b5302d42a2d0184f38d538c8a197f33b52a50bd4f7bcfe990bce3036/lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621", size = 3594072, upload-time = "2026-05-18T19:17:12.714Z" },
{ url = "https://files.pythonhosted.org/packages/61/9d/2e2f7d876349f45e0f3e29f72da311668853d59b58d473a2dea4f0160135/lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28", size = 4025469, upload-time = "2026-05-18T19:17:50.566Z" },
{ url = "https://files.pythonhosted.org/packages/b0/d5/570e6390e4110331e6208b2ba83d1482cc9146808ee118b22824a34c1070/lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b", size = 3667640, upload-time = "2026-05-19T19:22:48.293Z" },
{ url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821, upload-time = "2026-05-18T19:17:42.068Z" },
{ url = "https://files.pythonhosted.org/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252, upload-time = "2026-05-18T19:17:47.897Z" },
{ url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" },
{ url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" },
{ url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" },
{ url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" },
{ url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" },
{ url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171, upload-time = "2026-05-18T19:18:52.779Z" },
{ url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" },
{ url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" },
{ url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" },
{ url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" },
{ url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" },
{ url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" },
{ url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" },
{ url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" },
{ url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" },
{ url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610, upload-time = "2026-05-19T19:22:50.843Z" },
{ url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" },
{ url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" },
{ url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" },
{ url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" },
{ url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" },
{ url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" },
{ url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" },
{ url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" },
{ url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" },
{ url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" },
{ url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" },
{ url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" },
{ url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" },
{ url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" },
{ url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" },
{ url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" },
{ url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" },
{ url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" },
{ url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" },
{ url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" },
{ url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" },
{ url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" },
{ url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" },
{ url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" },
{ url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" },
{ url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" },
{ url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" },
{ url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" },
{ url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" },
{ url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" },
{ url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" },
{ url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" },
{ url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" },
{ url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" },
{ url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" },
{ url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" },
{ url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" },
{ url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" },
{ url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" },
{ url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" },
{ url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" },
{ url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" },
{ url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" },
{ url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" },
{ url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" },
{ url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" },
{ url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" },
{ url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" },
{ url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" },
{ url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" },
{ url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" },
{ url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" },
{ url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" },
{ url = "https://files.pythonhosted.org/packages/b5/32/86a3f0f724a3a402d4627937a7fc27b160e45e7012b4adf47f6e1e844511/lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e", size = 3930127, upload-time = "2026-05-18T19:19:02.27Z" },
{ url = "https://files.pythonhosted.org/packages/40/44/d832e82af08723761556d004b1d04d281c09f9a8cecd7d3148548c9941a3/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004", size = 4210769, upload-time = "2026-05-18T19:20:41.427Z" },
{ url = "https://files.pythonhosted.org/packages/6d/39/0dc5949f759ed7d951e0bb8c2f2d9d7aca1908d22352fa84a8afd2ea54af/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e", size = 4318163, upload-time = "2026-05-18T19:20:44.702Z" },
{ url = "https://files.pythonhosted.org/packages/e6/fb/8ab3845fe046ba4cbf74536bcf6801a774b7caf4350de1c5d37f1f0a9e90/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2", size = 4250945, upload-time = "2026-05-18T19:20:47.385Z" },
{ url = "https://files.pythonhosted.org/packages/68/1b/7553ab136894374ffae8851ec06f98f511cd8e66246e41b6be059d0a7289/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf", size = 4401664, upload-time = "2026-05-18T19:20:50.489Z" },
{ url = "https://files.pythonhosted.org/packages/db/a4/441aee36c6f6b249823d20fd91f9be9ab89d7c5a8ae542a4a4ca6d342d56/lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84", size = 3508989, upload-time = "2026-05-18T19:18:38.158Z" },
]
[[package]]
name = "meetus"
version = "0.1.0"
source = { virtual = "." }
[package.dev-dependencies]
deprecated = [
{ name = "ollama" },
{ name = "pytesseract" },
]
doocus = [
{ name = "beautifulsoup4" },
{ name = "lxml" },
{ name = "openpyxl" },
{ name = "pillow" },
{ name = "pypdf" },
{ name = "python-docx" },
{ name = "python-pptx" },
{ name = "pyyaml" },
]
meetus = [
{ name = "ffmpeg-python" },
{ name = "opencv-python" },
]
ocr = [
{ name = "pytesseract" },
]
pdf-render = [
{ name = "pdf2image" },
]
[package.metadata]
[package.metadata.requires-dev]
deprecated = [
{ name = "ollama", specifier = ">=0.1.0" },
{ name = "pytesseract", specifier = ">=0.3.10" },
]
doocus = [
{ name = "beautifulsoup4", specifier = ">=4.12.0" },
{ name = "lxml", specifier = ">=5.0.0" },
{ name = "openpyxl", specifier = ">=3.1.0" },
{ name = "pillow", specifier = ">=10.0.0" },
{ name = "pypdf", specifier = ">=4.0.0" },
{ name = "python-docx", specifier = ">=1.1.0" },
{ name = "python-pptx", specifier = ">=0.6.23" },
{ name = "pyyaml", specifier = ">=6.0" },
]
meetus = [
{ name = "ffmpeg-python", specifier = ">=0.2.0" },
{ name = "opencv-python", specifier = ">=4.8.0" },
]
ocr = [{ name = "pytesseract", specifier = ">=0.3.10" }]
pdf-render = [{ name = "pdf2image", specifier = ">=1.17.0" }]
[[package]]
name = "numpy"
version = "2.2.6"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
{ url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" },
{ url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" },
{ url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" },
{ url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" },
{ url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" },
{ url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" },
{ url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
{ url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
{ url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
{ url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
{ url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
{ url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
{ url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
{ url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
{ url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
{ url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
{ url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
{ url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
{ url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
{ url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
{ url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
{ url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
{ url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
{ url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
{ url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
{ url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
{ url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
{ url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
{ url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
{ url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
{ url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
{ url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
{ url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
{ url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
{ url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
{ url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
{ url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
{ url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
{ url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
{ url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
{ url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
{ url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
{ url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
{ url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
{ url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
{ url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
{ url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
{ url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
{ url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
{ url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
{ url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
{ url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
]
[[package]]
name = "numpy"
version = "2.4.6"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.11.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" },
{ url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" },
{ url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" },
{ url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" },
{ url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" },
{ url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" },
{ url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" },
{ url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" },
{ url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" },
{ url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" },
{ url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" },
{ url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
{ url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
{ url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
{ url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
{ url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" },
{ url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" },
{ url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" },
{ url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" },
{ url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
{ url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
{ url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
{ url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
{ url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
{ url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
{ url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
{ url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
{ url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
{ url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
{ url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
{ url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
{ url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
{ url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
{ url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
{ url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
{ url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
{ url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
{ url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
{ url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
{ url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
{ url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
{ url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
{ url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
{ url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
{ url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
{ url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
{ url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
{ url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
{ url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
{ url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
{ url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
{ url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
{ url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
{ url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
{ url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
{ url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
{ url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
{ url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
{ url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
{ url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
{ url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
{ url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" },
{ url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" },
{ url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" },
{ url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" },
{ url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" },
{ url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" },
{ url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" },
]
[[package]]
name = "numpy"
version = "2.5.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.12'",
]
sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" },
{ url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" },
{ url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" },
{ url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" },
{ url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" },
{ url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" },
{ url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" },
{ url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" },
{ url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" },
{ url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" },
{ url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" },
{ url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" },
{ url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" },
{ url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" },
{ url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" },
{ url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" },
{ url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" },
{ url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" },
{ url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" },
{ url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" },
{ url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" },
{ url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" },
{ url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" },
{ url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" },
{ url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" },
{ url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" },
{ url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" },
{ url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" },
{ url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" },
{ url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" },
{ url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" },
{ url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" },
{ url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" },
{ url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" },
{ url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" },
{ url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" },
{ url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" },
{ url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" },
{ url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" },
{ url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" },
{ url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" },
{ url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" },
{ url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" },
]
[[package]]
name = "ollama"
version = "0.6.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/72/5f12423b6b39ca8430fbe56f77fcf4ef60f63067c7c4a2e30e200ed9ec16/ollama-0.6.2.tar.gz", hash = "sha256:936d55daa684f474364c098611c933626f8d6c7d67065c5b7ae0c477b508b07f", size = 53145, upload-time = "2026-04-29T21:21:15.018Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/ab/d6722beeb2d10f7a3b9ff49375708904fde18f82b5609a0bc4aeb5996a4d/ollama-0.6.2-py3-none-any.whl", hash = "sha256:3ad7daab28e5a973445c36a73882a3ef698c2ebb00e21e308652741577509f7d", size = 15115, upload-time = "2026-04-29T21:21:13.794Z" },
]
[[package]]
name = "opencv-python"
version = "5.0.0.93"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
{ name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/4c/a438d23e09ce2033c09f7b784ad2fbdb0adf529e434101ed28f142226f98/opencv_python-5.0.0.93.tar.gz", hash = "sha256:66aac3e5b5faa48d4025816592f3af19e4bfc2c68dec067bae2dbb4ca10aa9e2", size = 81802749, upload-time = "2026-07-02T06:59:53.815Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/75/76f6ade78f6102c61034f828e2a22616708df2c9504bc8d6af9dd8f73dc5/opencv_python-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:198a75138241810206a17c829dbcc40a7cb1841cda538ca86cbbfc6c7d95f898", size = 48322443, upload-time = "2026-07-02T05:50:25.466Z" },
{ url = "https://files.pythonhosted.org/packages/15/8c/bc1bda6aae69a32e9d84fc34153ba104cd25226861eb4aea33b2cea4860d/opencv_python-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:6bbc32f59e1b1a7db7b39c81f63d00625f041d333037fd8702f6da52cc39108b", size = 34782755, upload-time = "2026-07-02T05:51:30.556Z" },
{ url = "https://files.pythonhosted.org/packages/f4/8a/b04776ec45d2dea08a1b176f1829201db3515d4ed16c35f8fcc9fa7beb16/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2b4272e736836f66c2d176e43ab8101f3a00d45654916399f52e150c58981ac", size = 50614064, upload-time = "2026-07-02T06:53:22.604Z" },
{ url = "https://files.pythonhosted.org/packages/95/54/eb47866b94f2b5b42dde17644b78055ef1ee05aae59962c7290e55270803/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8b6d0a212253dd26ad338c812f1f23ca118fdf05a9c8c6b9444f161aa8c5881", size = 71064711, upload-time = "2026-07-02T06:54:13.148Z" },
{ url = "https://files.pythonhosted.org/packages/93/da/962579f1e703cbf8c5422fd1f576467dcb3b5b0b0b81c1471c979764353a/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:08d5d91d967b58d6db86073b2ad3eaef88ca4ebdfd45c9059bf59f5ded0c7ad2", size = 49798576, upload-time = "2026-07-02T06:54:33.781Z" },
{ url = "https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c8de2dec111122a02e8beb28e16c31904992dfd6186560b142a92c71403c1039", size = 73783032, upload-time = "2026-07-02T06:55:03.415Z" },
{ url = "https://files.pythonhosted.org/packages/e2/4b/edaf83b996ca5a1a3d8ccad485706b9c6d4742b13b9c4586bf1c1e7d9423/opencv_python-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:4b4b1a34c79bf8d3738e3cfe9a9e67b51a79663f6b692cbdad8c31f570da4157", size = 35564734, upload-time = "2026-07-02T05:49:57.704Z" },
{ url = "https://files.pythonhosted.org/packages/21/f0/9fa6e85cb10c8eb36a0222d27e50fe381b86ce49a55446bf39f491727564/opencv_python-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:f90ba04b8f73bc5c3814037699739f0156f597338a98f05956c684e7c3ca10d2", size = 44000345, upload-time = "2026-07-02T05:49:54.971Z" },
]
[[package]]
name = "openpyxl"
version = "3.1.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "et-xmlfile" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
]
[[package]]
name = "packaging"
version = "26.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]]
name = "pdf2image"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pillow" },
]
sdist = { url = "https://files.pythonhosted.org/packages/00/d8/b280f01045555dc257b8153c00dee3bc75830f91a744cd5f84ef3a0a64b1/pdf2image-1.17.0.tar.gz", hash = "sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57", size = 12811, upload-time = "2024-01-07T20:33:01.965Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/33/61766ae033518957f877ab246f87ca30a85b778ebaad65b7f74fa7e52988/pdf2image-1.17.0-py3-none-any.whl", hash = "sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2", size = 11618, upload-time = "2024-01-07T20:32:59.957Z" },
]
[[package]]
name = "pillow"
version = "12.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" },
{ url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" },
{ url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" },
{ url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" },
{ url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" },
{ url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" },
{ url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" },
{ url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" },
{ url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" },
{ url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" },
{ url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" },
{ url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" },
{ url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" },
{ url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" },
{ url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" },
{ url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" },
{ url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" },
{ url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
{ url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
{ url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
{ url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
{ url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
{ url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
{ url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
{ url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
{ url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
{ url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
{ url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
{ url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
{ url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
{ url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
{ url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
{ url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
{ url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
{ url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
{ url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
{ url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
{ url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
{ url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
{ url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
{ url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
{ url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
{ url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
{ url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
{ url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
{ url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
{ url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
{ url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
{ url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
{ url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
{ url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
{ url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
{ url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
{ url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
{ url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
{ url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
{ url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
{ url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
{ url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
{ url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
{ url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
{ url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" },
{ url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" },
{ url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
{ url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
{ url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
{ url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
{ url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
{ url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
{ url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
{ url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" },
{ url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
{ url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
{ url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
{ url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
{ url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
{ url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
{ url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
{ url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" },
{ url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" },
{ url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" },
{ url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" },
]
[[package]]
name = "pydantic"
version = "2.13.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
]
[[package]]
name = "pydantic-core"
version = "2.46.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" },
{ url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" },
{ url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" },
{ url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" },
{ url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" },
{ url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" },
{ url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" },
{ url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" },
{ url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" },
{ url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" },
{ url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" },
{ url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" },
{ url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" },
{ url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" },
{ url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
{ url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
{ url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
{ url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
{ url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
{ url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
{ url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
{ url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
{ url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
{ url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
{ url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
{ url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
{ url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
{ url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
{ url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
{ url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
{ url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
{ url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
{ url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
{ url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
{ url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
{ url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
{ url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
{ url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
{ url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
{ url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
{ url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
{ url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
{ url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
{ url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
{ url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
{ url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
{ url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
{ url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
{ url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
{ url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
{ url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
{ url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
{ url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
{ url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
{ url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
{ url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
{ url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
{ url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
{ url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
{ url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
{ url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
{ url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
{ url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
{ url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
{ url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
{ url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
{ url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
{ url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
{ url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
{ url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
{ url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
{ url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
{ url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
]
[[package]]
name = "pypdf"
version = "6.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/03/72/7dfd5ff1c9c37de97a731701f51af091325f123d9d4270361c9c69e4431f/pypdf-6.14.2.tar.gz", hash = "sha256:7873f502fe4385e79539b21d872392dc0c4e3714327c15881cbc7fbfd1f95b25", size = 6491182, upload-time = "2026-06-23T14:18:30.859Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/e6/136aa8993a2ae7214e0b0ef2edaa0d2e08d1d4e4982635b08a835ff31ec8/pypdf-6.14.2-py3-none-any.whl", hash = "sha256:3f07891af76dc002657e04993ab9b4de81de29f9013b9761d0b7968bff12e946", size = 349514, upload-time = "2026-06-23T14:18:28.867Z" },
]
[[package]]
name = "pytesseract"
version = "0.3.13"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "pillow" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/a6/7d679b83c285974a7cb94d739b461fa7e7a9b17a3abfd7bf6cbc5c2394b0/pytesseract-0.3.13.tar.gz", hash = "sha256:4bf5f880c99406f52a3cfc2633e42d9dc67615e69d8a509d74867d3baddb5db9", size = 17689, upload-time = "2024-08-16T02:33:56.762Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/33/8312d7ce74670c9d39a532b2c246a853861120486be9443eebf048043637/pytesseract-0.3.13-py3-none-any.whl", hash = "sha256:7a99c6c2ac598360693d83a416e36e0b33a67638bb9d77fdcac094a3589d4b34", size = 14705, upload-time = "2024-08-16T02:36:10.09Z" },
]
[[package]]
name = "python-docx"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "lxml" },
{ name = "typing-extensions" },
]
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" }
wheels = [
{ 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" },
]
[[package]]
name = "python-pptx"
version = "1.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "lxml" },
{ name = "pillow" },
{ name = "typing-extensions" },
{ name = "xlsxwriter" },
]
sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297, upload-time = "2024-08-07T17:33:37.772Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "soupsieve"
version = "2.8.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "xlsxwriter"
version = "3.2.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940, upload-time = "2025-09-16T00:16:21.63Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" },
]