everalgo-parser 0.2.1__tar.gz → 0.3.0rc2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/.gitignore +3 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/CHANGELOG.md +22 -1
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/PKG-INFO +12 -13
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/README.md +8 -8
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/docs/architecture.md +13 -7
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/pyproject.toml +3 -6
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/src/everalgo/parser/__init__.py +11 -9
- everalgo_parser-0.3.0rc2/src/everalgo/parser/_replies.py +51 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/src/everalgo/parser/_utils.py +7 -78
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/src/everalgo/parser/audio.py +24 -22
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/src/everalgo/parser/document.py +64 -66
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/src/everalgo/parser/image.py +82 -88
- everalgo_parser-0.3.0rc2/src/everalgo/parser/prompts/_reply_format.py +15 -0
- everalgo_parser-0.3.0rc2/src/everalgo/parser/prompts/en/__init__.py +15 -0
- everalgo_parser-0.3.0rc2/src/everalgo/parser/prompts/en/audio.py +24 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/src/everalgo/parser/prompts/en/document.py +24 -3
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/src/everalgo/parser/prompts/en/image.py +24 -5
- everalgo_parser-0.3.0rc2/src/everalgo/parser/prompts/zh/__init__.py +15 -0
- everalgo_parser-0.3.0rc2/src/everalgo/parser/prompts/zh/audio.py +24 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/src/everalgo/parser/prompts/zh/document.py +16 -3
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/src/everalgo/parser/prompts/zh/image.py +17 -6
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/src/everalgo/parser/url.py +24 -7
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.htm +1 -1
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.html +1 -1
- everalgo_parser-0.3.0rc2/tests/parser/test_audio.py +67 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/parser/test_dispatch.py +6 -5
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/parser/test_document.py +132 -22
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/parser/test_e2e_all_formats.py +22 -13
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/parser/test_e2e_pdf_openrouter.py +7 -10
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/parser/test_image.py +63 -44
- everalgo_parser-0.3.0rc2/tests/parser/test_input_errors.py +43 -0
- everalgo_parser-0.3.0rc2/tests/parser/test_prompt_rendering.py +53 -0
- everalgo_parser-0.3.0rc2/tests/parser/test_prompt_split.py +337 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/parser/test_url.py +4 -31
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/parser/test_video.py +1 -2
- everalgo_parser-0.2.1/src/everalgo/parser/prompts/en/__init__.py +0 -13
- everalgo_parser-0.2.1/src/everalgo/parser/prompts/en/audio.py +0 -11
- everalgo_parser-0.2.1/src/everalgo/parser/prompts/zh/__init__.py +0 -13
- everalgo_parser-0.2.1/src/everalgo/parser/prompts/zh/audio.py +0 -14
- everalgo_parser-0.2.1/tests/parser/test_audio.py +0 -48
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/LICENSE +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/src/everalgo/parser/prompts/__init__.py +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/src/everalgo/parser/py.typed +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/src/everalgo/parser/video.py +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.bmp +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.docx +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.eml +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.jpg +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.pdf +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.png +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.pptx +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.svg +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.tiff +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.txt +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.wav +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.webp +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/fixtures/sample.xlsx +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/parser/__init__.py +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/parser/test_public_api.py +0 -0
- {everalgo_parser-0.2.1 → everalgo_parser-0.3.0rc2}/tests/parser/test_utils.py +0 -0
|
@@ -7,8 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
<!-- git-cliff-unreleased-start -->
|
|
9
9
|
## [Unreleased]
|
|
10
|
+
|
|
10
11
|
<!-- git-cliff-unreleased-end -->
|
|
11
12
|
|
|
13
|
+
## [0.3.0rc2] - 2026-10-13
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Invalid caller input now raises `everalgo.utils.InvalidInputError` (a `ValueError` subclass, so existing handlers keep working) instead of a bare `ValueError`: a `RawFile` with neither content nor uri, with empty content, with an unsupported extension or modality, a uri that returns an empty body or a content type the parser cannot read. Each message names the field and what to set instead; a uri it quotes is cut to `scheme://host[:port]/path`, so a signed URL's query string, fragment and credentials never reach the message.
|
|
18
|
+
- Requires `everalgo-core>=0.6.0rc2` (the floor was `0.6.0rc1`): the package imports `everalgo.utils.InvalidInputError`, which `0.6.0rc1` does not define.
|
|
19
|
+
|
|
20
|
+
## [0.3.0rc1] - 2026-10-13
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **BREAKING:** every LLM call follows the new `LLMClient` contract: it passes `response_format=_TextReply`, a strict pydantic model of one field, `{"text": "..."}`, reads the `str` `achat` returns and validates it with `everalgo.llm.parse_reply`. A reply that is not that object — plain text, a missing or extra key, a non-string `text` — raises `everalgo.llm.LLMOutputError` from the image, PDF, HTML, Office and audio parsers. Two algorithm-level fallbacks keep their behaviour and now also cover an invalid reply: a tall-image merge that fails or comes back empty joins the slices' OCR (`metadata["merge_fallback"] = "deterministic_join"`), and an inline email image whose OCR fails gets the `[image: <name>, OCR failed]` placeholder. A custom client passed as `llm` must accept the mandatory keyword and return the reply text. See `everalgo-core` for the contract.
|
|
25
|
+
- **BREAKING:** every call sends a system message and a user message instead of one user message. The system message is the prompt's rules, unchanged, followed by a `<reply_format>` block asking for `{"text": "..."}` (shared by the English and Chinese sets, `prompts/_reply_format.py`); `PROMPT_FOR_PICTURE`, `PROMPT_FOR_MERGE`, `PROMPT_FOR_FILE`, `PROMPT_FOR_HTML` and `PROMPT_FOR_AUDIO` are now these system messages. The user message is the input alone: the image, PDF or audio part with no text beside it; for HTML the cleaned page in `<html_content>` (new `HTML_USER_PROMPT`, slot `{{html}}`) instead of after a `---` line; for the tall-image merge the OCR texts, each in a `<part number="N">`, in `<ocr_parts count="N">` (new `MERGE_USER_PROMPT`, slots `{{count}}` and `{{parts}}`) instead of `=== Part N ===` sections in `PROMPT_FOR_MERGE`'s removed `{{parts_text}}` slot.
|
|
26
|
+
- **BREAKING:** `ParsedContent.metadata` no longer carries `model` or `finish_reason`: `achat` returns only the reply text, and the model actually served is the host's to record. The image, audio, PDF, HTML and Office results keep their other keys.
|
|
27
|
+
- **BREAKING:** operators call `LLMClient.achat`, the new name of the protocol method; a custom client passed as `llm` must implement `achat` instead of `chat`. See `everalgo-core` for the contract.
|
|
28
|
+
- The URL parser fetches through `everalgo.utils.fetch_uri`, which moved to `everalgo-core`; the `ValueError` for a scheme other than `http` / `https` now reads `fetch_uri: only http and https are supported, got scheme 'file'` instead of `fetch_uri: only http/https supported, got scheme='file'. file:// and other local-filesystem URIs are rejected by design (...)`, so match on the exception type, not the text.
|
|
29
|
+
- `everalgo-parser` no longer declares `httpx` itself: it makes no HTTP call of its own any more, and `everalgo-core` declares `httpx>=0.27` for the fetch.
|
|
30
|
+
|
|
12
31
|
## [0.2.1] - 2026-06-15
|
|
13
32
|
|
|
14
33
|
### Changed
|
|
@@ -80,6 +99,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
80
99
|
- Optional `cairosvg >= 2.7.0` via `[svg]` extra.
|
|
81
100
|
- System: LibreOffice (`soffice` on PATH) for Office document parsing.
|
|
82
101
|
|
|
83
|
-
[Unreleased]: https://github.com/EverMind-AI/EverAlgo/compare/everalgo-parser/v0.
|
|
102
|
+
[Unreleased]: https://github.com/EverMind-AI/EverAlgo/compare/everalgo-parser/v0.3.0rc2...HEAD
|
|
103
|
+
[0.3.0rc2]: https://github.com/EverMind-AI/EverAlgo/compare/everalgo-parser/v0.3.0rc1...everalgo-parser/v0.3.0rc2
|
|
104
|
+
[0.3.0rc1]: https://github.com/EverMind-AI/EverAlgo/compare/everalgo-parser/v0.2.1...everalgo-parser/v0.3.0rc1
|
|
84
105
|
[0.2.1]: https://github.com/EverMind-AI/EverAlgo/compare/everalgo-parser/v0.2.0...everalgo-parser/v0.2.1
|
|
85
106
|
[0.2.0]: https://github.com/EverMind-AI/EverAlgo/releases/tag/everalgo-parser/v0.2.0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: everalgo-parser
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: EverAlgo parser:
|
|
3
|
+
Version: 0.3.0rc2
|
|
4
|
+
Summary: EverAlgo parser: image, audio, document, and URL parsing into ParsedContent.
|
|
5
5
|
Project-URL: Homepage, https://github.com/EverMind-AI/EverAlgo
|
|
6
6
|
Project-URL: Repository, https://github.com/EverMind-AI/EverAlgo
|
|
7
7
|
Project-URL: Issues, https://github.com/EverMind-AI/EverAlgo/issues
|
|
@@ -20,8 +20,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
20
20
|
Requires-Python: >=3.12
|
|
21
21
|
Requires-Dist: asgiref>=3.8.0
|
|
22
22
|
Requires-Dist: beautifulsoup4>=4.12.0
|
|
23
|
-
Requires-Dist: everalgo-core<2.0.0,>=0.
|
|
24
|
-
Requires-Dist: httpx>=0.27.0
|
|
23
|
+
Requires-Dist: everalgo-core<2.0.0,>=0.6.0rc2
|
|
25
24
|
Requires-Dist: pillow>=10.0.0
|
|
26
25
|
Provides-Extra: svg
|
|
27
26
|
Requires-Dist: cairosvg>=2.7.0; extra == 'svg'
|
|
@@ -29,25 +28,24 @@ Description-Content-Type: text/markdown
|
|
|
29
28
|
|
|
30
29
|
# everalgo-parser
|
|
31
30
|
|
|
32
|
-
Multimodal parsing — image / audio / document /
|
|
31
|
+
Multimodal parsing — image / audio / document / URL raw inputs into `ParsedContent`; video is deferred. Used by `everalgo-knowledge` for file ingestion and by EverOS step 1 for inline parsing.
|
|
33
32
|
|
|
34
33
|
See the umbrella project: [EverAlgo monorepo](../../README.md) and the architecture document at [`docs/concepts/architecture.md`](../../docs/concepts/architecture.md).
|
|
35
34
|
|
|
36
35
|
## Quick start
|
|
37
36
|
|
|
38
37
|
```python
|
|
39
|
-
from everalgo.llm.
|
|
40
|
-
from everalgo.llm.providers.openai_compat import OpenAICompatClient
|
|
38
|
+
from everalgo.llm.openai_compat import OpenAICompatClient
|
|
41
39
|
from everalgo.parser import aparse, RawFile
|
|
42
40
|
|
|
43
41
|
# Create an LLM client per call (or share one instance). The parser uses
|
|
44
42
|
# OpenAI-compatible clients; OpenRouter is the reference deployment
|
|
45
43
|
# (Gemini multimodal via OpenRouter passthrough).
|
|
46
|
-
client = OpenAICompatClient(
|
|
44
|
+
client = OpenAICompatClient(
|
|
47
45
|
model="google/gemini-3-flash-preview",
|
|
48
46
|
api_key="sk-or-v1-...",
|
|
49
47
|
base_url="https://openrouter.ai/api/v1",
|
|
50
|
-
)
|
|
48
|
+
)
|
|
51
49
|
|
|
52
50
|
# Bytes-in: caller already hydrated the file.
|
|
53
51
|
parsed = await aparse(RawFile(content=pdf_bytes, extension="pdf"), llm=client)
|
|
@@ -97,14 +95,15 @@ The parser detects `soffice` via `shutil.which("soffice")` and the canonical mac
|
|
|
97
95
|
|
|
98
96
|
- `aparse(...)` is async; `parse(...)` is the sync bridge via `asgiref.async_to_sync`.
|
|
99
97
|
- Prompts live as module-level string constants under `prompts/{en,zh}/<operator>.py` ([AGENTS.md §5](../../AGENTS.md#5-code-style)). Swap languages by re-binding the constant at startup.
|
|
100
|
-
- The
|
|
101
|
-
-
|
|
98
|
+
- The parser owns no durable state and never reads caller-selected local paths. URL parsing performs HTTP(S) I/O, and Office conversion writes files only inside a parser-managed temporary directory that is removed before the call returns.
|
|
99
|
+
- Every LLM call sends the prompt's rules as the system message and the input — the file part, or the cleaned HTML / the OCR texts in XML-style tags — as the user message, and asks for one `{"text": "..."}` object (the `everalgo-core` LLM contract has no free-text tier). `ParsedContent.text` is that `text`.
|
|
100
|
+
- No retry / metrics inside operators — the client's errors propagate unchanged and an invalid reply raises `everalgo.llm.LLMOutputError`; let the caller wrap. The only fallbacks are algorithmic: a failed tall-image merge joins the slices' OCR, and a failed inline-image OCR leaves a placeholder in the email.
|
|
102
101
|
|
|
103
102
|
## Security notes for callers
|
|
104
103
|
|
|
105
104
|
`everalgo-parser` is a stateless library; production safety properties belong to the calling service. The points below are easy to miss and worth wiring into your integration:
|
|
106
105
|
|
|
107
|
-
- **SSRF — `url.aparse` fetches arbitrary HTTP(S) URIs.** `fetch_uri` rejects non-http(s) schemes (no `file://`, no `gopher://`) but does **not** block private / link-local / loopback IPs (e.g. `127.0.0.1`, `10.0.0.0/8`, `169.254.169.254` cloud metadata, IPv6 ULA). If callers may pass attacker-controlled URLs, resolve the hostname first and reject private ranges before invoking `aparse`, or front the parser with an egress proxy that does the filtering.
|
|
106
|
+
- **SSRF — `url.aparse` fetches arbitrary HTTP(S) URIs.** `everalgo.utils.fetch_uri` rejects non-http(s) schemes (no `file://`, no `gopher://`) but does **not** block private / link-local / loopback IPs (e.g. `127.0.0.1`, `10.0.0.0/8`, `169.254.169.254` cloud metadata, IPv6 ULA). If callers may pass attacker-controlled URLs, resolve the hostname first and reject private ranges before invoking `aparse`, or front the parser with an egress proxy that does the filtering.
|
|
108
107
|
- **Decompression-bomb cap is set at import time.** `everalgo/parser/__init__.py` pins `PIL.Image.MAX_IMAGE_PIXELS = 100_000_000` (10000×10000) — Pillow will raise `DecompressionBombError` above that. Re-assign after import if your workload genuinely needs larger images.
|
|
109
108
|
- **Office conversion runs LibreOffice as a subprocess.** `document._convert_to_pdf_via_soffice` validates the `extension` parameter (alphanumeric, ≤8 chars) before writing the temp file, but the LibreOffice attack surface itself is large. Run the host with a non-root user and treat untrusted office docs as you would any other complex binary input.
|
|
110
109
|
- **`cairosvg` (optional `[svg]` extra) is LGPL-3.0-or-later.** EverAlgo itself is Apache-2.0. LGPLv3 is compatible with Apache-2.0 when `cairosvg` is consumed as an unmodified library through its public Python API (which is what the SVG path does); if you statically vendor or modify `cairosvg`, LGPLv3 §4 / §5 obligations apply to that derivative. Not installing the `[svg]` extra removes the dependency entirely.
|
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
# everalgo-parser
|
|
2
2
|
|
|
3
|
-
Multimodal parsing — image / audio / document /
|
|
3
|
+
Multimodal parsing — image / audio / document / URL raw inputs into `ParsedContent`; video is deferred. Used by `everalgo-knowledge` for file ingestion and by EverOS step 1 for inline parsing.
|
|
4
4
|
|
|
5
5
|
See the umbrella project: [EverAlgo monorepo](../../README.md) and the architecture document at [`docs/concepts/architecture.md`](../../docs/concepts/architecture.md).
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
9
9
|
```python
|
|
10
|
-
from everalgo.llm.
|
|
11
|
-
from everalgo.llm.providers.openai_compat import OpenAICompatClient
|
|
10
|
+
from everalgo.llm.openai_compat import OpenAICompatClient
|
|
12
11
|
from everalgo.parser import aparse, RawFile
|
|
13
12
|
|
|
14
13
|
# Create an LLM client per call (or share one instance). The parser uses
|
|
15
14
|
# OpenAI-compatible clients; OpenRouter is the reference deployment
|
|
16
15
|
# (Gemini multimodal via OpenRouter passthrough).
|
|
17
|
-
client = OpenAICompatClient(
|
|
16
|
+
client = OpenAICompatClient(
|
|
18
17
|
model="google/gemini-3-flash-preview",
|
|
19
18
|
api_key="sk-or-v1-...",
|
|
20
19
|
base_url="https://openrouter.ai/api/v1",
|
|
21
|
-
)
|
|
20
|
+
)
|
|
22
21
|
|
|
23
22
|
# Bytes-in: caller already hydrated the file.
|
|
24
23
|
parsed = await aparse(RawFile(content=pdf_bytes, extension="pdf"), llm=client)
|
|
@@ -68,14 +67,15 @@ The parser detects `soffice` via `shutil.which("soffice")` and the canonical mac
|
|
|
68
67
|
|
|
69
68
|
- `aparse(...)` is async; `parse(...)` is the sync bridge via `asgiref.async_to_sync`.
|
|
70
69
|
- Prompts live as module-level string constants under `prompts/{en,zh}/<operator>.py` ([AGENTS.md §5](../../AGENTS.md#5-code-style)). Swap languages by re-binding the constant at startup.
|
|
71
|
-
- The
|
|
72
|
-
-
|
|
70
|
+
- The parser owns no durable state and never reads caller-selected local paths. URL parsing performs HTTP(S) I/O, and Office conversion writes files only inside a parser-managed temporary directory that is removed before the call returns.
|
|
71
|
+
- Every LLM call sends the prompt's rules as the system message and the input — the file part, or the cleaned HTML / the OCR texts in XML-style tags — as the user message, and asks for one `{"text": "..."}` object (the `everalgo-core` LLM contract has no free-text tier). `ParsedContent.text` is that `text`.
|
|
72
|
+
- No retry / metrics inside operators — the client's errors propagate unchanged and an invalid reply raises `everalgo.llm.LLMOutputError`; let the caller wrap. The only fallbacks are algorithmic: a failed tall-image merge joins the slices' OCR, and a failed inline-image OCR leaves a placeholder in the email.
|
|
73
73
|
|
|
74
74
|
## Security notes for callers
|
|
75
75
|
|
|
76
76
|
`everalgo-parser` is a stateless library; production safety properties belong to the calling service. The points below are easy to miss and worth wiring into your integration:
|
|
77
77
|
|
|
78
|
-
- **SSRF — `url.aparse` fetches arbitrary HTTP(S) URIs.** `fetch_uri` rejects non-http(s) schemes (no `file://`, no `gopher://`) but does **not** block private / link-local / loopback IPs (e.g. `127.0.0.1`, `10.0.0.0/8`, `169.254.169.254` cloud metadata, IPv6 ULA). If callers may pass attacker-controlled URLs, resolve the hostname first and reject private ranges before invoking `aparse`, or front the parser with an egress proxy that does the filtering.
|
|
78
|
+
- **SSRF — `url.aparse` fetches arbitrary HTTP(S) URIs.** `everalgo.utils.fetch_uri` rejects non-http(s) schemes (no `file://`, no `gopher://`) but does **not** block private / link-local / loopback IPs (e.g. `127.0.0.1`, `10.0.0.0/8`, `169.254.169.254` cloud metadata, IPv6 ULA). If callers may pass attacker-controlled URLs, resolve the hostname first and reject private ranges before invoking `aparse`, or front the parser with an egress proxy that does the filtering.
|
|
79
79
|
- **Decompression-bomb cap is set at import time.** `everalgo/parser/__init__.py` pins `PIL.Image.MAX_IMAGE_PIXELS = 100_000_000` (10000×10000) — Pillow will raise `DecompressionBombError` above that. Re-assign after import if your workload genuinely needs larger images.
|
|
80
80
|
- **Office conversion runs LibreOffice as a subprocess.** `document._convert_to_pdf_via_soffice` validates the `extension` parameter (alphanumeric, ≤8 chars) before writing the temp file, but the LibreOffice attack surface itself is large. Run the host with a non-root user and treat untrusted office docs as you would any other complex binary input.
|
|
81
81
|
- **`cairosvg` (optional `[svg]` extra) is LGPL-3.0-or-later.** EverAlgo itself is Apache-2.0. LGPLv3 is compatible with Apache-2.0 when `cairosvg` is consumed as an unmodified library through its public Python API (which is what the SVG path does); if you statically vendor or modify `cairosvg`, LGPLv3 §4 / §5 obligations apply to that derivative. Not installing the `[svg]` extra removes the dependency entirely.
|
|
@@ -12,7 +12,8 @@ GitHub 默认渲染下面的 Mermaid 图。
|
|
|
12
12
|
packages/everalgo-parser/
|
|
13
13
|
├── src/everalgo/parser/
|
|
14
14
|
│ ├── __init__.py ← 顶层 facade:dispatch + re-export 公开类型
|
|
15
|
-
│ ├── _utils.py ← 私有助手:
|
|
15
|
+
│ ├── _utils.py ← 私有助手:aspect-ratio / html-clean
|
|
16
|
+
│ ├── _replies.py ← 回复模型 `_TextReply{text}` + 唯一 LLM 调用点 `_call_llm_for_text`
|
|
16
17
|
│ ├── image.py ← IMAGE modality 算子(含长图分块 + BMP/TIFF 转码 + SVG 栅格化)
|
|
17
18
|
│ ├── audio.py ← AUDIO modality 算子
|
|
18
19
|
│ ├── document.py ← PDF / HTML / EMAIL / Office(LibreOffice)四合一
|
|
@@ -20,8 +21,9 @@ packages/everalgo-parser/
|
|
|
20
21
|
│ ├── video.py ← stub,等 ADR
|
|
21
22
|
│ └── prompts/
|
|
22
23
|
│ ├── __init__.py
|
|
23
|
-
│ ├──
|
|
24
|
-
│
|
|
24
|
+
│ ├── _reply_format.py ← 所有 system prompt 末尾共用的 `<reply_format>` 块
|
|
25
|
+
│ ├── en/{image,audio,document}.py ← system:PROMPT_FOR_{PICTURE,MERGE,AUDIO,FILE,HTML};user:MERGE_USER_PROMPT / HTML_USER_PROMPT
|
|
26
|
+
│ └── zh/{image,audio,document}.py ← 中文对偶(同名常量、同样拆分)
|
|
25
27
|
├── tests/
|
|
26
28
|
│ ├── fixtures/ ← 17 个真实 sample.* 文件
|
|
27
29
|
│ └── parser/
|
|
@@ -31,6 +33,8 @@ packages/everalgo-parser/
|
|
|
31
33
|
│ ├── test_e2e_all_formats.py
|
|
32
34
|
│ ├── test_e2e_pdf_openrouter.py
|
|
33
35
|
│ ├── test_image.py
|
|
36
|
+
│ ├── test_prompt_rendering.py
|
|
37
|
+
│ ├── test_prompt_split.py
|
|
34
38
|
│ ├── test_public_api.py
|
|
35
39
|
│ ├── test_url.py
|
|
36
40
|
│ ├── test_utils.py
|
|
@@ -86,7 +90,7 @@ flowchart TD
|
|
|
86
90
|
- `uri=http(s)://` + `content=b""` → 走 `url.aparse`(内部抓取 + 按 Content-Type 再分派)。
|
|
87
91
|
- 已经有 `content`:**extension 优先**,extension 解析不到时**用 mime 兜底**(`get_modality_from_mime`)。这样 design.md §2.1 的 `RawFile(uri=..., mime="application/pdf")` 走 URL 抓取后能再走到 PDF handler。
|
|
88
92
|
- 子模块内部派发用 extension(`_MIME_MAP[ext]` 查 mime)。当顶层用 mime 解出 modality 时,自动 `model_copy(extension=get_extension_from_mime(mime))` 给子模块。
|
|
89
|
-
- `file://` 在 `
|
|
93
|
+
- `file://` 在 `everalgo.utils.fetch_uri` 里直接拒绝(AGENTS.md §1)。
|
|
90
94
|
- `Modality.URL` 是顶层 dispatch 产生的"来源标签",不出现在 extension 派发表里。
|
|
91
95
|
|
|
92
96
|
---
|
|
@@ -149,7 +153,7 @@ graph LR
|
|
|
149
153
|
| `text` | `str` | 提取后的主文本(Markdown / 纯文本) |
|
|
150
154
|
| `modality` | `Modality` | 来源分类 |
|
|
151
155
|
| `mime` | `str` | 源 MIME(无则空串) |
|
|
152
|
-
| `metadata` | `dict[str, Any]` | 按模态填:`
|
|
156
|
+
| `metadata` | `dict[str, Any]` | 按模态填:`tall_image_parts` / `merge_fallback` / `inline_image_ocr` / `og_tags` / `intermediate_pdf_bytes` 等。不含 `model` / `finish_reason`:`achat` 只返回文本,实际服务的模型由宿主记录(core `docs/concepts/llm-contract.md` §6.4) |
|
|
153
157
|
|
|
154
158
|
### `Modality` 枚举(StrEnum,9 值)
|
|
155
159
|
|
|
@@ -163,6 +167,8 @@ IMAGE PDF AUDIO DOCUMENT HTML EMAIL URL DIRECT UNKNOWN
|
|
|
163
167
|
|
|
164
168
|
## 6. 各 modality 调用链
|
|
165
169
|
|
|
170
|
+
所有 LLM 调用同一形态:system = 规则 + `<reply_format>`(要求回复 `{"text": "..."}`),user = 文件 part(图片 / PDF / 音频)或带标签的文本(`<html_content>` / `<ocr_parts>`);回复经 `parse_reply(_TextReply, ...)` 校验后取 `text`,坏回复抛 `LLMOutputError`。例外:长图 merge 与邮件内嵌图 OCR 失败(含坏回复)走算法层兜底(拼接 / 占位符),不抛。
|
|
171
|
+
|
|
166
172
|
### 6.1 IMAGE(`image.aparse`)
|
|
167
173
|
|
|
168
174
|
```mermaid
|
|
@@ -181,7 +187,7 @@ flowchart LR
|
|
|
181
187
|
J --> K[per-part OCR<br/>N x LLM calls]
|
|
182
188
|
K --> L{N > 1?}
|
|
183
189
|
L -- no --> M[return part]
|
|
184
|
-
L -- yes --> N[LLM merge<br/>PROMPT_FOR_MERGE]
|
|
190
|
+
L -- yes --> N[LLM merge<br/>PROMPT_FOR_MERGE + ocr_parts<br/>失败 / 空 → 确定性拼接]
|
|
185
191
|
I --> R[ParsedContent]
|
|
186
192
|
M --> R
|
|
187
193
|
N --> R
|
|
@@ -193,7 +199,7 @@ flowchart LR
|
|
|
193
199
|
flowchart TD
|
|
194
200
|
A[bytes + ext] --> M{Modality from ext}
|
|
195
201
|
M -- PDF --> P[image_url part + PROMPT_FOR_FILE → 1 LLM call]
|
|
196
|
-
M -- HTML --> H[decode_bytes → clean_html_for_llm bs4 →
|
|
202
|
+
M -- HTML --> H[decode_bytes → clean_html_for_llm bs4 → html_content + PROMPT_FOR_HTML → 1 LLM call]
|
|
197
203
|
M -- EMAIL --> E[stdlib email parse → headers + body]
|
|
198
204
|
E --> EI{inline images?}
|
|
199
205
|
EI -- yes --> EOCR[per-image OCR via LLM<br/>PROMPT_FOR_PICTURE]
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "everalgo-parser"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "EverAlgo parser:
|
|
7
|
+
version = "0.3.0rc2"
|
|
8
|
+
description = "EverAlgo parser: image, audio, document, and URL parsing into ParsedContent."
|
|
9
9
|
requires-python = ">=3.12"
|
|
10
10
|
license = "Apache-2.0"
|
|
11
11
|
readme = "README.md"
|
|
@@ -20,13 +20,10 @@ classifiers = [
|
|
|
20
20
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
21
21
|
]
|
|
22
22
|
dependencies = [
|
|
23
|
-
"everalgo-core>=0.
|
|
23
|
+
"everalgo-core>=0.6.0rc2,<2.0.0",
|
|
24
24
|
"asgiref>=3.8.0",
|
|
25
25
|
"pillow>=10.0.0",
|
|
26
26
|
"beautifulsoup4>=4.12.0",
|
|
27
|
-
# The URL parser uses httpx directly for ``http``/``https`` fetches; declare
|
|
28
|
-
# explicitly rather than relying on the transitive pull-in via openai SDK.
|
|
29
|
-
"httpx>=0.27.0",
|
|
30
27
|
]
|
|
31
28
|
|
|
32
29
|
[project.urls]
|
|
@@ -25,6 +25,7 @@ from everalgo.types import (
|
|
|
25
25
|
get_modality,
|
|
26
26
|
get_modality_from_mime,
|
|
27
27
|
)
|
|
28
|
+
from everalgo.utils import InvalidInputError
|
|
28
29
|
|
|
29
30
|
if TYPE_CHECKING:
|
|
30
31
|
from everalgo.llm import LLMClient
|
|
@@ -63,7 +64,7 @@ async def aparse(raw_file: RawFile, *, llm: LLMClient) -> ParsedContent:
|
|
|
63
64
|
- ``AUDIO`` → ``audio.aparse``
|
|
64
65
|
- ``PDF`` / ``DOCUMENT`` / ``HTML`` / ``EMAIL`` → ``document.aparse``
|
|
65
66
|
- ``DIRECT`` → no LLM; bytes are UTF-8-decoded inline.
|
|
66
|
-
- Anything else → ``
|
|
67
|
+
- Anything else → ``InvalidInputError``.
|
|
67
68
|
|
|
68
69
|
When dispatch resolves a modality via ``mime`` but the submodule
|
|
69
70
|
internally requires ``extension`` (image / audio / document all do for
|
|
@@ -87,10 +88,9 @@ async def aparse(raw_file: RawFile, *, llm: LLMClient) -> ParsedContent:
|
|
|
87
88
|
return await url.aparse(raw_file, llm=llm)
|
|
88
89
|
|
|
89
90
|
if not raw_file.content:
|
|
90
|
-
raise
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"hydrated bytes."
|
|
91
|
+
raise InvalidInputError(
|
|
92
|
+
"RawFile has neither content nor an http(s) uri, so there is nothing to parse: "
|
|
93
|
+
"pass the file bytes in content, or an http(s) uri for the URL parser to fetch"
|
|
94
94
|
)
|
|
95
95
|
|
|
96
96
|
# Resolve modality: extension wins; fall back to mime.
|
|
@@ -98,8 +98,9 @@ async def aparse(raw_file: RawFile, *, llm: LLMClient) -> ParsedContent:
|
|
|
98
98
|
if modality is Modality.UNKNOWN and raw_file.mime:
|
|
99
99
|
modality = get_modality_from_mime(raw_file.mime)
|
|
100
100
|
if modality is Modality.UNKNOWN:
|
|
101
|
-
raise
|
|
102
|
-
f"
|
|
101
|
+
raise InvalidInputError(
|
|
102
|
+
f"RawFile extension={raw_file.extension!r} mime={raw_file.mime!r} map to no known modality: "
|
|
103
|
+
"set a supported extension or a matching mime before calling"
|
|
103
104
|
)
|
|
104
105
|
|
|
105
106
|
# Ensure ``extension`` is populated for downstream submodules that key
|
|
@@ -118,8 +119,9 @@ async def aparse(raw_file: RawFile, *, llm: LLMClient) -> ParsedContent:
|
|
|
118
119
|
modality=Modality.DIRECT,
|
|
119
120
|
mime=routed.mime,
|
|
120
121
|
)
|
|
121
|
-
raise
|
|
122
|
-
f"
|
|
122
|
+
raise InvalidInputError(
|
|
123
|
+
f"RawFile modality {modality.value!r} (extension={raw_file.extension!r}, mime={raw_file.mime!r}) "
|
|
124
|
+
"is not parseable: this parser handles image, audio, document and plain-text files only"
|
|
123
125
|
)
|
|
124
126
|
|
|
125
127
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""The reply model every parser prompt asks for, and the one LLM callsite every parser call goes through.
|
|
2
|
+
|
|
3
|
+
Each parser output is free text (an OCR, a transcription, a merged OCR text). The contract has no free-text tier, so
|
|
4
|
+
every prompt asks for the ``{"text": "..."}`` object instead (``docs/concepts/llm-contract.md`` §6.1). One call, one
|
|
5
|
+
validation, no retry: the host decides (§4, §5).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from everalgo.llm import LLMMessage, StrictOutputModel, parse_reply
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from everalgo.llm import ContentPart, LLMClient
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _TextReply(StrictOutputModel):
|
|
19
|
+
"""The ``{"text": "..."}`` skeleton every parser prompt's ``<reply_format>`` shows."""
|
|
20
|
+
|
|
21
|
+
text: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def _call_llm_for_text(
|
|
25
|
+
llm: LLMClient,
|
|
26
|
+
*,
|
|
27
|
+
system: str,
|
|
28
|
+
user: str | list[ContentPart],
|
|
29
|
+
operator: str,
|
|
30
|
+
temperature: float | None = None,
|
|
31
|
+
max_tokens: int | None = None,
|
|
32
|
+
) -> str:
|
|
33
|
+
"""Send the rules as the system message and the input as the user message; return the reply's ``text``.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
llm: The client to call.
|
|
37
|
+
system: The prompt's rules, ending with its ``<reply_format>``.
|
|
38
|
+
user: The input: the tagged text, or the file's content parts.
|
|
39
|
+
operator: Names the call in a parse error, e.g. ``"parser pdf"``.
|
|
40
|
+
temperature: Passed through; None leaves it to the host.
|
|
41
|
+
max_tokens: Passed through; None leaves it to the host.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The reply's ``text``, verbatim; empty when the model returned an empty string.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
LLMOutputError: If the reply is not a valid ``{"text": "..."}`` object.
|
|
48
|
+
"""
|
|
49
|
+
messages = [LLMMessage(role="system", content=system), LLMMessage(role="user", content=user)]
|
|
50
|
+
reply = await llm.achat(messages, response_format=_TextReply, temperature=temperature, max_tokens=max_tokens)
|
|
51
|
+
return parse_reply(_TextReply, reply, operator=operator).text
|
|
@@ -1,101 +1,30 @@
|
|
|
1
1
|
"""Internal parser helpers.
|
|
2
2
|
|
|
3
|
-
- ``decode_bytes`` — multi-encoding decoder for unknown-charset bytes.
|
|
4
3
|
- ``check_aspect_ratio`` / ``split_image_with_overlap`` — long-image (tall
|
|
5
4
|
screenshot) handling for the image parser.
|
|
6
5
|
- ``clean_html_content`` — strict text extraction (drops all tags).
|
|
7
6
|
- ``clean_html_for_llm`` — preserves structural tags inside ``<body>``,
|
|
8
7
|
strips known-noise tags and useless attributes, returns HTML.
|
|
9
|
-
- ``fetch_uri`` — HTTP(S) fetch for the URL parser. Adapted from an
|
|
10
|
-
upstream internal URL extractor reference implementation.
|
|
11
8
|
"""
|
|
12
9
|
|
|
13
10
|
from __future__ import annotations
|
|
14
11
|
|
|
15
|
-
import logging
|
|
16
|
-
import math
|
|
17
|
-
import re
|
|
18
|
-
from io import BytesIO
|
|
19
|
-
from urllib.parse import urlparse
|
|
20
|
-
|
|
21
|
-
import httpx
|
|
22
|
-
from bs4 import BeautifulSoup, Comment
|
|
23
|
-
from PIL import Image
|
|
24
|
-
|
|
25
|
-
logger = logging.getLogger(__name__)
|
|
26
|
-
|
|
27
12
|
__all__ = [
|
|
28
13
|
"check_aspect_ratio",
|
|
29
14
|
"clean_html_content",
|
|
30
15
|
"clean_html_for_llm",
|
|
31
|
-
"decode_bytes",
|
|
32
|
-
"fetch_uri",
|
|
33
16
|
"split_image_with_overlap",
|
|
34
17
|
]
|
|
35
18
|
|
|
19
|
+
import logging
|
|
20
|
+
import math
|
|
21
|
+
import re
|
|
22
|
+
from io import BytesIO
|
|
36
23
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
async def fetch_uri(uri: str, *, timeout: float = 30.0) -> tuple[bytes, str]:
|
|
41
|
-
"""Fetch ``http``/``https`` URI and return ``(content_bytes, content_type)``.
|
|
42
|
-
|
|
43
|
-
Adapted from an upstream internal URL extractor reference implementation;
|
|
44
|
-
rejects non-http(s) schemes per AGENTS.md §1 (no filesystem access).
|
|
45
|
-
|
|
46
|
-
Parameters
|
|
47
|
-
----------
|
|
48
|
-
uri : str
|
|
49
|
-
Origin URI. Must be ``http://`` or ``https://``.
|
|
50
|
-
timeout : float, optional
|
|
51
|
-
Per-request timeout in seconds (default 30s).
|
|
52
|
-
|
|
53
|
-
Returns:
|
|
54
|
-
-------
|
|
55
|
-
tuple[bytes, str]
|
|
56
|
-
Response body and the upstream ``Content-Type`` (lower-cased,
|
|
57
|
-
param-stripped, e.g. ``text/html``). Empty string when the upstream
|
|
58
|
-
omits the header.
|
|
24
|
+
from bs4 import BeautifulSoup, Comment
|
|
25
|
+
from PIL import Image
|
|
59
26
|
|
|
60
|
-
|
|
61
|
-
------
|
|
62
|
-
ValueError
|
|
63
|
-
URI is missing or uses a non-http(s) scheme (``file:`` / ``data:``
|
|
64
|
-
/ ...).
|
|
65
|
-
httpx.HTTPError
|
|
66
|
-
Network failure surfaces unchanged so callers can apply their own
|
|
67
|
-
retry / fallback policy (the library does not retry — see ADR-012).
|
|
68
|
-
"""
|
|
69
|
-
if not uri:
|
|
70
|
-
raise ValueError("fetch_uri: empty uri")
|
|
71
|
-
parsed = urlparse(uri)
|
|
72
|
-
if parsed.scheme not in ("http", "https"):
|
|
73
|
-
raise ValueError(
|
|
74
|
-
f"fetch_uri: only http/https supported, got scheme={parsed.scheme!r}. "
|
|
75
|
-
f"file:// and other local-filesystem URIs are rejected by design "
|
|
76
|
-
f"(AGENTS.md §1: stateless library, no filesystem access)."
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
headers = {"User-Agent": _DEFAULT_USER_AGENT}
|
|
80
|
-
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True, headers=headers) as client:
|
|
81
|
-
response = await client.get(uri)
|
|
82
|
-
response.raise_for_status()
|
|
83
|
-
# Strip the charset/boundary parameters: ``text/html; charset=utf-8`` → ``text/html``.
|
|
84
|
-
content_type = response.headers.get("content-type", "").split(";")[0].strip().lower()
|
|
85
|
-
logger.debug(
|
|
86
|
-
"fetched uri=%s status=%d bytes=%d type=%s", uri, response.status_code, len(response.content), content_type
|
|
87
|
-
)
|
|
88
|
-
return response.content, content_type
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def decode_bytes(content: bytes) -> str:
|
|
92
|
-
"""Try several encodings before falling back to lossy UTF-8."""
|
|
93
|
-
for encoding in ("utf-8", "gbk", "gb2312", "latin-1"):
|
|
94
|
-
try:
|
|
95
|
-
return content.decode(encoding)
|
|
96
|
-
except (UnicodeDecodeError, LookupError):
|
|
97
|
-
continue
|
|
98
|
-
return content.decode("utf-8", errors="ignore")
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
99
28
|
|
|
100
29
|
|
|
101
30
|
def check_aspect_ratio(image_bytes: bytes, max_ratio: float = 10.0) -> tuple[bool, float]:
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Audio parser — ASR via multimodal LLM.
|
|
2
2
|
|
|
3
3
|
Sends the audio bytes as a single ``image_url`` data URI (OpenRouter / Gemini accepts
|
|
4
|
-
audio MIMEs through the same content-part shape)
|
|
4
|
+
audio MIMEs through the same content-part shape) as the user message, the rules as
|
|
5
|
+
the system message, and reads the transcription from the reply's ``text``.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
from __future__ import annotations
|
|
@@ -11,11 +12,14 @@ from typing import TYPE_CHECKING
|
|
|
11
12
|
|
|
12
13
|
from asgiref.sync import async_to_sync
|
|
13
14
|
|
|
14
|
-
from everalgo.llm import
|
|
15
|
+
from everalgo.llm import image_url_part_from_bytes
|
|
16
|
+
from everalgo.parser._replies import _call_llm_for_text
|
|
15
17
|
from everalgo.parser.prompts.en import PROMPT_FOR_AUDIO
|
|
16
18
|
from everalgo.types import Modality, ParsedContent
|
|
19
|
+
from everalgo.utils import InvalidInputError
|
|
17
20
|
|
|
18
21
|
if TYPE_CHECKING:
|
|
22
|
+
from everalgo.llm import LLMClient
|
|
19
23
|
from everalgo.types import RawFile
|
|
20
24
|
|
|
21
25
|
__all__ = ["aparse", "parse"]
|
|
@@ -36,35 +40,33 @@ _MIME_MAP: dict[str, str] = {
|
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
async def aparse(raw_file: RawFile, *, llm: LLMClient) -> ParsedContent:
|
|
39
|
-
"""Parse an audio ``RawFile`` into ``ParsedContent`` (transcription).
|
|
43
|
+
"""Parse an audio ``RawFile`` into ``ParsedContent`` (transcription).
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
InvalidInputError: If ``raw_file`` has no content or an unsupported extension.
|
|
47
|
+
LLMOutputError: If the reply is not a valid ``{"text": "..."}`` object.
|
|
48
|
+
"""
|
|
40
49
|
if not raw_file.content:
|
|
41
|
-
raise
|
|
50
|
+
raise InvalidInputError(
|
|
51
|
+
"RawFile.content is empty, there is nothing to transcribe: pass the audio bytes in content"
|
|
52
|
+
)
|
|
42
53
|
|
|
43
54
|
extension = raw_file.extension.lower().lstrip(".")
|
|
44
55
|
mime = _MIME_MAP.get(extension)
|
|
45
56
|
if mime is None:
|
|
46
|
-
raise
|
|
47
|
-
f"
|
|
57
|
+
raise InvalidInputError(
|
|
58
|
+
f"RawFile.extension {raw_file.extension!r} is not a supported audio extension: "
|
|
59
|
+
f"set it to one of {sorted(_MIME_MAP)}"
|
|
48
60
|
)
|
|
49
61
|
|
|
50
|
-
message = ChatMessage(
|
|
51
|
-
role="user",
|
|
52
|
-
content=[
|
|
53
|
-
TextPart(text=PROMPT_FOR_AUDIO),
|
|
54
|
-
image_url_part_from_bytes(raw_file.content, mime),
|
|
55
|
-
],
|
|
56
|
-
)
|
|
57
62
|
logger.debug("parsing audio extension=%s bytes=%d", extension, len(raw_file.content))
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
metadata={
|
|
64
|
-
"model": response.model,
|
|
65
|
-
"finish_reason": response.finish_reason,
|
|
66
|
-
},
|
|
63
|
+
text = await _call_llm_for_text(
|
|
64
|
+
llm,
|
|
65
|
+
system=PROMPT_FOR_AUDIO,
|
|
66
|
+
user=[image_url_part_from_bytes(raw_file.content, mime)],
|
|
67
|
+
operator="parser audio",
|
|
67
68
|
)
|
|
69
|
+
return ParsedContent(text=text, modality=Modality.AUDIO, mime=raw_file.mime or mime)
|
|
68
70
|
|
|
69
71
|
|
|
70
72
|
parse = async_to_sync(aparse)
|