ltcai 11.1.0 → 11.4.0
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.
- package/README.md +47 -54
- package/docs/CHANGELOG.md +94 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/FEATURE_AUDIT_v11.2.0.md +393 -0
- package/docs/LAYOUT_REBUILD_SPEC.md +9 -1
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -2
- package/docs/PERMISSION_MODE.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/architecture.md +6 -2
- package/docs/kg-schema.md +2 -2
- package/docs/v11.3.0_PLAN.md +202 -0
- package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/gates.py +125 -0
- package/lattice_brain/graph/_kg_common/__init__.py +287 -0
- package/lattice_brain/graph/_kg_common/extraction.py +516 -0
- package/lattice_brain/graph/_kg_common/relations.py +161 -0
- package/lattice_brain/graph/_kg_common/text.py +479 -0
- package/lattice_brain/graph/discovery_index/__init__.py +35 -0
- package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
- package/lattice_brain/graph/discovery_index/extract.py +137 -0
- package/lattice_brain/graph/discovery_index/scan.py +411 -0
- package/lattice_brain/graph/discovery_index/upsert.py +495 -0
- package/lattice_brain/graph/fusion.py +35 -4
- package/lattice_brain/graph/projection/__init__.py +42 -0
- package/lattice_brain/graph/projection/curation.py +500 -0
- package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +81 -485
- package/lattice_brain/graph/retrieval/__init__.py +54 -0
- package/lattice_brain/graph/retrieval/context.py +197 -0
- package/lattice_brain/graph/retrieval/graph_view.py +319 -0
- package/lattice_brain/graph/retrieval/hybrid.py +488 -0
- package/lattice_brain/graph/retrieval/maintenance.py +121 -0
- package/lattice_brain/graph/retrieval/signals.py +95 -0
- package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
- package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
- package/lattice_brain/graph/retrieval_vector/search.py +560 -0
- package/lattice_brain/graph/retrieval_vector/status.py +374 -0
- package/lattice_brain/graph/schema.py +9 -0
- package/lattice_brain/graph/store.py +9 -0
- package/lattice_brain/graph/vector_index/selector.py +32 -2
- package/lattice_brain/ingestion/__init__.py +130 -0
- package/lattice_brain/ingestion/_contract.py +90 -0
- package/lattice_brain/ingestion/constants.py +127 -0
- package/lattice_brain/ingestion/folder_scan.py +57 -0
- package/lattice_brain/ingestion/folders.py +258 -0
- package/lattice_brain/ingestion/hashing.py +26 -0
- package/lattice_brain/ingestion/jobs_api.py +107 -0
- package/lattice_brain/ingestion/models.py +80 -0
- package/lattice_brain/ingestion/pipeline.py +486 -0
- package/lattice_brain/ingestion/quality.py +209 -0
- package/lattice_brain/ingestion/routing.py +295 -0
- package/lattice_brain/multimodal/__init__.py +164 -0
- package/lattice_brain/multimodal/audio.py +77 -0
- package/lattice_brain/multimodal/common.py +118 -0
- package/lattice_brain/{multimodal.py → multimodal/images.py} +22 -262
- package/lattice_brain/multimodal/ports.py +169 -0
- package/lattice_brain/multimodal/video.py +410 -0
- package/lattice_brain/portability/__init__.py +90 -0
- package/lattice_brain/portability/_contract.py +42 -0
- package/lattice_brain/portability/backups.py +338 -0
- package/lattice_brain/portability/bundles.py +136 -0
- package/lattice_brain/portability/constants.py +93 -0
- package/lattice_brain/portability/fsops.py +138 -0
- package/lattice_brain/portability/service.py +41 -0
- package/lattice_brain/portability/sharing.py +714 -0
- package/lattice_brain/runtime/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/sealed_box.py +244 -0
- package/lattice_brain/synthesis.py +24 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/brain_intelligence.py +4 -0
- package/latticeai/api/chat.py +11 -0
- package/latticeai/api/chat_helpers.py +16 -3
- package/latticeai/api/chat_hybrid.py +32 -1
- package/latticeai/api/chronicle.py +63 -0
- package/latticeai/api/features.py +70 -0
- package/latticeai/api/local_files.py +102 -0
- package/latticeai/api/portability.py +39 -4
- package/latticeai/api/review_queue.py +126 -0
- package/latticeai/api/search.py +16 -2
- package/latticeai/core/agent/__init__.py +93 -0
- package/latticeai/core/agent/_contract.py +79 -0
- package/latticeai/core/agent/context.py +57 -0
- package/latticeai/core/agent/deps.py +125 -0
- package/latticeai/core/agent/execution.py +622 -0
- package/latticeai/core/agent/planning.py +145 -0
- package/latticeai/core/agent/recovery.py +157 -0
- package/latticeai/core/agent/runtime.py +210 -0
- package/latticeai/core/agent/verification.py +231 -0
- package/latticeai/core/config.py +4 -1
- package/latticeai/core/context_builder.py +6 -3
- package/latticeai/core/embedding_providers/__init__.py +151 -0
- package/latticeai/core/embedding_providers/base.py +199 -0
- package/latticeai/core/embedding_providers/captions.py +162 -0
- package/latticeai/core/embedding_providers/profiles.py +126 -0
- package/latticeai/core/embedding_providers/text.py +350 -0
- package/latticeai/core/embedding_providers/vision.py +352 -0
- package/latticeai/core/file_generation/__init__.py +115 -0
- package/latticeai/core/file_generation/bundles.py +76 -0
- package/latticeai/core/file_generation/extraction.py +154 -0
- package/latticeai/core/file_generation/inference.py +235 -0
- package/latticeai/core/file_generation/orchestration.py +152 -0
- package/latticeai/core/file_generation/prompting.py +117 -0
- package/latticeai/core/file_generation/repair.py +114 -0
- package/latticeai/core/file_generation/sanitize.py +61 -0
- package/latticeai/core/file_generation/validation.py +201 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +152 -0
- package/latticeai/core/model_compat.py +73 -2
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/integrations/telegram_bot/__init__.py +123 -0
- package/latticeai/integrations/telegram_bot/__main__.py +17 -0
- package/latticeai/integrations/telegram_bot/config.py +86 -0
- package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
- package/latticeai/integrations/telegram_bot/flows.py +478 -0
- package/latticeai/integrations/telegram_bot/helpers.py +322 -0
- package/latticeai/integrations/telegram_bot/screens.py +394 -0
- package/latticeai/models/model_providers.py +12 -4
- package/latticeai/models/router/__init__.py +88 -0
- package/latticeai/models/router/_contract.py +66 -0
- package/latticeai/models/router/branding.py +56 -0
- package/latticeai/models/router/catalog.py +69 -0
- package/latticeai/models/router/documents.py +199 -0
- package/latticeai/models/router/errors.py +37 -0
- package/latticeai/models/router/generation.py +258 -0
- package/latticeai/models/router/loading.py +291 -0
- package/latticeai/models/router/local_models.py +85 -0
- package/latticeai/models/router/registry.py +147 -0
- package/latticeai/runtime/build_phases/__init__.py +82 -0
- package/latticeai/runtime/build_phases/features.py +407 -0
- package/latticeai/runtime/build_phases/foundation.py +555 -0
- package/latticeai/runtime/build_phases/web.py +492 -0
- package/latticeai/runtime/chat_wiring.py +4 -0
- package/latticeai/runtime/feature_toggle_wiring.py +163 -0
- package/latticeai/runtime/router_registration.py +11 -0
- package/latticeai/runtime/runtime_context.py +1 -0
- package/latticeai/services/app_context.py +8 -0
- package/latticeai/services/architecture_readiness.py +48 -19
- package/latticeai/services/automation_intelligence.py +22 -2
- package/latticeai/services/brain_intelligence/__init__.py +58 -0
- package/latticeai/services/brain_intelligence/_contract.py +71 -0
- package/latticeai/services/brain_intelligence/consistency.py +193 -0
- package/latticeai/services/brain_intelligence/constants.py +47 -0
- package/latticeai/services/brain_intelligence/digest.py +258 -0
- package/latticeai/services/brain_intelligence/health.py +331 -0
- package/latticeai/services/brain_intelligence/proposals.py +264 -0
- package/latticeai/services/brain_intelligence/sampling.py +84 -0
- package/latticeai/services/brain_intelligence/service.py +48 -0
- package/latticeai/services/chronicle.py +557 -0
- package/latticeai/services/command_center.py +10 -4
- package/latticeai/services/feature_toggles.py +502 -0
- package/latticeai/services/folder_watch.py +122 -1
- package/latticeai/services/hybrid_chat.py +56 -5
- package/latticeai/services/interop_bridges.py +978 -0
- package/latticeai/services/memory_service/__init__.py +52 -0
- package/latticeai/services/memory_service/_contract.py +100 -0
- package/latticeai/services/memory_service/brief.py +431 -0
- package/latticeai/services/memory_service/constants.py +57 -0
- package/latticeai/services/memory_service/maintenance.py +138 -0
- package/latticeai/services/memory_service/manager.py +186 -0
- package/latticeai/services/memory_service/proof.py +136 -0
- package/latticeai/services/memory_service/recall.py +225 -0
- package/latticeai/services/memory_service/service.py +48 -0
- package/latticeai/services/memory_service/stores.py +110 -0
- package/latticeai/services/model_capability_registry.py +434 -261
- package/latticeai/services/model_catalog.py +95 -61
- package/latticeai/services/model_recommendation.py +18 -11
- package/latticeai/services/model_runtime/__init__.py +322 -0
- package/latticeai/services/model_runtime/cloud.py +87 -0
- package/latticeai/services/model_runtime/download.py +282 -0
- package/latticeai/services/model_runtime/engines.py +341 -0
- package/latticeai/services/model_runtime/loading.py +178 -0
- package/latticeai/services/model_runtime/service.py +129 -0
- package/latticeai/services/model_runtime/state.py +131 -0
- package/latticeai/services/model_runtime/status.py +255 -0
- package/latticeai/services/multimodal_ports.py +26 -1
- package/latticeai/services/obsidian_bridge.py +16 -25
- package/latticeai/services/product_readiness.py +15 -7
- package/latticeai/services/search_service.py +149 -2
- package/latticeai/services/tool_dispatch.py +4 -0
- package/latticeai/setup/auto_setup.py +27 -30
- package/latticeai/setup/wizard/__init__.py +126 -0
- package/latticeai/setup/wizard/catalog.py +172 -0
- package/latticeai/setup/wizard/detect.py +323 -0
- package/latticeai/setup/wizard/install.py +348 -0
- package/latticeai/setup/wizard/paths.py +168 -0
- package/latticeai/setup/wizard/plans.py +74 -0
- package/latticeai/setup/wizard/recommend.py +320 -0
- package/package.json +6 -2
- package/scripts/bump_version.py +14 -0
- package/scripts/capture_release_evidence.mjs +33 -21
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_i18n_namespace_coverage.mjs +41 -4
- package/scripts/check_max_file_lines.mjs +102 -0
- package/scripts/check_release_evidence_bound.mjs +30 -15
- package/scripts/check_screenshot_pixel_delta.py +34 -4
- package/scripts/check_server_i18n.mjs +2 -0
- package/scripts/generate_rust_parity_fixtures.py +562 -0
- package/scripts/lib/mock_server_fingerprint.mjs +94 -0
- package/scripts/release_screen_claims.json +44 -2
- package/scripts/verify_hf_model_registry.py +253 -218
- package/src-tauri/Cargo.lock +361 -3
- package/src-tauri/Cargo.toml +6 -1
- package/src-tauri/src/backend.rs +349 -0
- package/src-tauri/src/folder.rs +33 -0
- package/src-tauri/src/main.rs +97 -399
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +41 -37
- package/static/app/assets/Act-yYpYnn0v.js +1 -0
- package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
- package/static/app/assets/{Brain-CzCsI1mi.js → Brain-C1HBN0Wf.js} +2 -2
- package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
- package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
- package/static/app/assets/Capture-CFIRsFNE.js +1 -0
- package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
- package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
- package/static/app/assets/Library-DwO3yZST.js +1 -0
- package/static/app/assets/{LivingBrain-BXMWIK_2.js → LivingBrain-Jn1GK0-S.js} +1 -1
- package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
- package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
- package/static/app/assets/System-DW8F-2xL.js +1 -0
- package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
- package/static/app/assets/{bot-4BvN07ux.js → bot-IM_E_Y12.js} +1 -1
- package/static/app/assets/brain-Ci1CkWjM.js +1 -0
- package/static/app/assets/{button-CDjtnAoU.js → button-COwyqfHM.js} +1 -1
- package/static/app/assets/circle-check-DfInj-qD.js +1 -0
- package/static/app/assets/{circle-pause-D_RMn7tp.js → circle-pause-DEM4A1Y5.js} +1 -1
- package/static/app/assets/{circle-play-B5OpB8ae.js → circle-play-C9djDuLd.js} +1 -1
- package/static/app/assets/{cpu-BIlWInHf.js → cpu-DFdo1gw-.js} +1 -1
- package/static/app/assets/{download-BtjXfL3z.js → download-SnJL6oqk.js} +1 -1
- package/static/app/assets/{folder-open-DefMpxI2.js → folder-open-CqZeDkjE.js} +1 -1
- package/static/app/assets/{hard-drive-BQ8NZVkw.js → hard-drive-j1jJXYYf.js} +1 -1
- package/static/app/assets/{index-vtEfYvQY.css → index-BLPb5lmE.css} +1 -1
- package/static/app/assets/index-_u5iUHDr.js +10 -0
- package/static/app/assets/input-B0lPdRQZ.js +1 -0
- package/static/app/assets/link-2-CoFbooHS.js +1 -0
- package/static/app/assets/{permissionCopy-BqZ5tsgL.js → permissionCopy-BsyLxtao.js} +1 -1
- package/static/app/assets/primitives-DEbN-d6p.js +1 -0
- package/static/app/assets/search-BybIWPNd.js +1 -0
- package/static/app/assets/{share-2-D5zg_0fY.js → share-2-CVtZ_ewX.js} +1 -1
- package/static/app/assets/{shield-alert-B5pZzkUb.js → shield-alert-CBi2GNWM.js} +1 -1
- package/static/app/assets/{textarea-nEVIweKY.js → textarea-DNMpB5ih.js} +1 -1
- package/static/app/assets/{useFocusTrap-Cm99AHlz.js → useFocusTrap-C83t3GXF.js} +1 -1
- package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
- package/static/app/assets/{useQuery-Dm__N6bL.js → useQuery-Dcp1OChy.js} +1 -1
- package/static/app/assets/utils-BlZr7Pd4.js +4 -0
- package/static/app/assets/workspace-jJY4RuAV.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/graph/_kg_common.py +0 -1331
- package/lattice_brain/graph/discovery_index.py +0 -1141
- package/lattice_brain/graph/retrieval.py +0 -1120
- package/lattice_brain/graph/retrieval_vector.py +0 -1293
- package/lattice_brain/ingestion.py +0 -1377
- package/lattice_brain/portability.py +0 -1210
- package/latticeai/core/agent.py +0 -1412
- package/latticeai/core/embedding_providers.py +0 -1196
- package/latticeai/core/file_generation.py +0 -1047
- package/latticeai/integrations/telegram_bot.py +0 -1390
- package/latticeai/models/router.py +0 -1007
- package/latticeai/runtime/build_phases.py +0 -1422
- package/latticeai/services/brain_intelligence.py +0 -967
- package/latticeai/services/memory_service.py +0 -1177
- package/latticeai/services/model_runtime.py +0 -1281
- package/latticeai/setup/wizard.py +0 -1277
- package/static/app/assets/Act-D0HWqtn0.js +0 -1
- package/static/app/assets/AdminConsole-D-QDW-A4.js +0 -1
- package/static/app/assets/BrainHome-Btns-_TA.js +0 -2
- package/static/app/assets/BrainSignals-2dHQNkns.js +0 -1
- package/static/app/assets/Capture-CT8v1StE.js +0 -1
- package/static/app/assets/CommandPalette-DoLXC2KH.js +0 -1
- package/static/app/assets/Library-DDoxFE5c.js +0 -1
- package/static/app/assets/ProductFlow-DOYf7JIs.js +0 -1
- package/static/app/assets/ReviewCard-COQsqidK.js +0 -3
- package/static/app/assets/System-BRllvYXd.js +0 -1
- package/static/app/assets/arrow-left-DnyMzss-.js +0 -1
- package/static/app/assets/brain-uMb_5hnO.js +0 -1
- package/static/app/assets/index-0AvoEBzJ.js +0 -10
- package/static/app/assets/input-B_5ZJ9oy.js +0 -1
- package/static/app/assets/primitives-CVwew78r.js +0 -1
- package/static/app/assets/search-DkhnOKZt.js +0 -1
- package/static/app/assets/utils-DcDMoZIe.js +0 -4
- package/static/app/assets/workspace-LtRRSKTf.js +0 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""A recording as a memory — transcribed through the injected port, or not.
|
|
2
|
+
|
|
3
|
+
The whole module is three small pieces because that is all an audio memory is:
|
|
4
|
+
the recording's facts, one attempt at turning it into words, and an honest
|
|
5
|
+
score for how much of it a typed question can reach afterwards. Without a
|
|
6
|
+
transcriber the memory is still kept and simply says so.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from .ports import MultimodalPorts
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class AudioFacts:
|
|
19
|
+
"""A recording, its transcript, and how honestly we got one."""
|
|
20
|
+
|
|
21
|
+
path: str
|
|
22
|
+
#: ``ok`` | ``unavailable`` | ``failed`` | ``supplied``
|
|
23
|
+
transcription_status: str = "unavailable"
|
|
24
|
+
transcript: str = ""
|
|
25
|
+
detail: str = ""
|
|
26
|
+
segments: List[Dict[str, Any]] = field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def searchable(self) -> bool:
|
|
30
|
+
return bool(self.transcript)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def transcribe_audio(
|
|
34
|
+
path: str,
|
|
35
|
+
*,
|
|
36
|
+
ports: Optional[MultimodalPorts] = None,
|
|
37
|
+
transcript: Optional[str] = None,
|
|
38
|
+
) -> AudioFacts:
|
|
39
|
+
"""Transcribe a recording through the injected port, or say why not.
|
|
40
|
+
|
|
41
|
+
``transcript`` lets a caller that already has text (a phone's own
|
|
42
|
+
dictation, ``VoiceCaptureService``) skip the local model entirely. An
|
|
43
|
+
absent transcriber yields ``transcription_status="unavailable"`` and an
|
|
44
|
+
empty transcript — the recording is still remembered by title and path,
|
|
45
|
+
and the result never claims it is searchable.
|
|
46
|
+
"""
|
|
47
|
+
ports = ports or MultimodalPorts()
|
|
48
|
+
supplied = str(transcript or "").strip()
|
|
49
|
+
if supplied:
|
|
50
|
+
return AudioFacts(path=str(path), transcription_status="supplied", transcript=supplied)
|
|
51
|
+
if ports.transcriber is None:
|
|
52
|
+
return AudioFacts(
|
|
53
|
+
path=str(path),
|
|
54
|
+
transcription_status="unavailable",
|
|
55
|
+
detail="no local transcriber is configured",
|
|
56
|
+
)
|
|
57
|
+
try:
|
|
58
|
+
text = str(ports.transcriber(str(path)) or "").strip()
|
|
59
|
+
except Exception as exc: # noqa: BLE001 — a broken transcriber is a state
|
|
60
|
+
return AudioFacts(path=str(path), transcription_status="failed", detail=str(exc))
|
|
61
|
+
if not text:
|
|
62
|
+
return AudioFacts(
|
|
63
|
+
path=str(path),
|
|
64
|
+
transcription_status="failed",
|
|
65
|
+
detail="the transcriber returned no text",
|
|
66
|
+
)
|
|
67
|
+
return AudioFacts(path=str(path), transcription_status="ok", transcript=text)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def audio_quality_score(facts: AudioFacts) -> Dict[str, Any]:
|
|
71
|
+
"""How much of this recording is actually retrievable later."""
|
|
72
|
+
if not facts.transcript:
|
|
73
|
+
return {"score": 0.0, "reasons": ["no_transcript"]}
|
|
74
|
+
# A transcript is text: length is the only honest extra signal here, and
|
|
75
|
+
# the text pipeline scores the wording itself downstream.
|
|
76
|
+
score = 0.5 + 0.5 * min(1.0, len(facts.transcript) / 400.0)
|
|
77
|
+
return {"score": round(score, 4), "reasons": ["transcript"]}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Modality taxonomy, size budgets, and the pure helpers every door shares.
|
|
2
|
+
|
|
3
|
+
The tables here are the module's own decisions (``.mp4`` is video unless the
|
|
4
|
+
capture surface says otherwise), deliberately kept ahead of the platform's
|
|
5
|
+
``mimetypes`` answer. Nothing in this file touches a model, a decoder, or the
|
|
6
|
+
graph, which is what lets images, audio and video all import it without
|
|
7
|
+
importing each other.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import mimetypes
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import List, Optional
|
|
16
|
+
|
|
17
|
+
# ── modality taxonomy ────────────────────────────────────────────────────────
|
|
18
|
+
MODALITY_TEXT = "text"
|
|
19
|
+
MODALITY_IMAGE = "image"
|
|
20
|
+
MODALITY_AUDIO = "audio"
|
|
21
|
+
MODALITY_VIDEO = "video"
|
|
22
|
+
|
|
23
|
+
IMAGE_EXTENSIONS = frozenset(
|
|
24
|
+
{".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff", ".heic"}
|
|
25
|
+
)
|
|
26
|
+
# Containers that are *only* ever audio. ``.mp4``/``.webm`` are deliberately
|
|
27
|
+
# absent: by extension alone they are video, and a voice memo recorded in one
|
|
28
|
+
# of them arrives through an explicit audio MIME type (or through
|
|
29
|
+
# ``VoiceCaptureService``, where the user already said "this is a memo").
|
|
30
|
+
# ``.mid``/``.midi`` are listed for a second reason: CPython's *built-in* mime
|
|
31
|
+
# table has neither, so ``mimetypes`` answers "audio/midi" only on a host that
|
|
32
|
+
# ships a system mime file (macOS reads /etc/apache2/mime.types; a slim Linux
|
|
33
|
+
# container has nothing). Leaving them to the fallback let the platform decide
|
|
34
|
+
# what a MIDI file is — a module table exists precisely so it does not.
|
|
35
|
+
AUDIO_EXTENSIONS = frozenset(
|
|
36
|
+
{".m4a", ".mp3", ".wav", ".aac", ".flac", ".ogg", ".opus", ".mid", ".midi"}
|
|
37
|
+
)
|
|
38
|
+
VIDEO_EXTENSIONS = frozenset({".mp4", ".webm", ".mov", ".mkv", ".avi", ".m4v"})
|
|
39
|
+
#: Subtitle/caption files a video may arrive with. Same basename, so a
|
|
40
|
+
#: ``standup.mp4`` next to a ``standup.srt`` is one memory, not two.
|
|
41
|
+
SUBTITLE_EXTENSIONS = ("srt", "vtt")
|
|
42
|
+
|
|
43
|
+
#: Why a video is recognized and still refused — surfaced to the caller. In
|
|
44
|
+
#: 11.1.0 the reason was *scope* (nothing was implemented). Since 11.2.0 the
|
|
45
|
+
#: implementation exists and the only remaining reason is a **runtime** one:
|
|
46
|
+
#: this machine has no ``ffmpeg``, and inventing frames is not an option.
|
|
47
|
+
VIDEO_UNAVAILABLE_DETAIL = (
|
|
48
|
+
"video ingestion needs ffmpeg on this machine and none was found; the file "
|
|
49
|
+
"was not stored (install ffmpeg to enable keyframe extraction)"
|
|
50
|
+
)
|
|
51
|
+
#: Kept under its 11.1.0 name so existing importers keep working; the reason it
|
|
52
|
+
#: carries has changed from "out of scope" to "unavailable on this machine".
|
|
53
|
+
VIDEO_OUT_OF_SCOPE = VIDEO_UNAVAILABLE_DETAIL
|
|
54
|
+
|
|
55
|
+
#: Longest OCR/caption body kept on the node (a screenshot is not a novel).
|
|
56
|
+
MAX_INDEX_TEXT_CHARS = 20_000
|
|
57
|
+
#: Summary column budget, matching every other ingest door in the graph.
|
|
58
|
+
SUMMARY_CHARS = 500
|
|
59
|
+
#: Fixed-width chunking for OCR bodies that outgrow the summary.
|
|
60
|
+
IMAGE_CHUNK_CHARS = 900
|
|
61
|
+
#: Longest edge of the stored thumbnail, in pixels.
|
|
62
|
+
THUMBNAIL_EDGE = 96
|
|
63
|
+
#: A thumbnail is a UI affordance, not an archive — drop it past this size.
|
|
64
|
+
MAX_THUMBNAIL_CHARS = 24_000
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def detect_modality(
|
|
68
|
+
path: Optional[str] = None, mime_type: Optional[str] = None
|
|
69
|
+
) -> str:
|
|
70
|
+
"""``text`` | ``image`` | ``audio`` | ``video`` for one candidate file.
|
|
71
|
+
|
|
72
|
+
The declared MIME type wins when it carries a usable top-level type: the
|
|
73
|
+
capture surface saw the bytes, this function only sees a name. Otherwise
|
|
74
|
+
the extension decides, and an unknown extension is ``text`` so existing
|
|
75
|
+
behaviour is untouched.
|
|
76
|
+
"""
|
|
77
|
+
declared = str(mime_type or "").strip().lower().split(";")[0].split("/")[0]
|
|
78
|
+
if declared in {MODALITY_IMAGE, MODALITY_AUDIO, MODALITY_VIDEO}:
|
|
79
|
+
return declared
|
|
80
|
+
# The extension tables come before ``mimetypes`` on purpose: they are where
|
|
81
|
+
# this module's decisions live (``.mp4`` is video unless someone who saw
|
|
82
|
+
# the bytes says otherwise), and the stdlib table varies by platform.
|
|
83
|
+
suffix = Path(str(path or "")).suffix.lower()
|
|
84
|
+
if suffix in IMAGE_EXTENSIONS:
|
|
85
|
+
return MODALITY_IMAGE
|
|
86
|
+
if suffix in AUDIO_EXTENSIONS:
|
|
87
|
+
return MODALITY_AUDIO
|
|
88
|
+
if suffix in VIDEO_EXTENSIONS:
|
|
89
|
+
return MODALITY_VIDEO
|
|
90
|
+
if path:
|
|
91
|
+
guessed, _ = mimetypes.guess_type(str(path))
|
|
92
|
+
top = str(guessed or "").split("/")[0]
|
|
93
|
+
if top in {MODALITY_IMAGE, MODALITY_AUDIO, MODALITY_VIDEO}:
|
|
94
|
+
return top
|
|
95
|
+
return MODALITY_TEXT
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _sha256_file(path: Path) -> str:
|
|
99
|
+
digest = hashlib.sha256()
|
|
100
|
+
with path.open("rb") as handle:
|
|
101
|
+
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
|
102
|
+
digest.update(block)
|
|
103
|
+
return digest.hexdigest()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _sha256_text(text: str) -> str:
|
|
107
|
+
return hashlib.sha256(str(text).encode("utf-8", "ignore")).hexdigest()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _split_index_text(text: str) -> List[str]:
|
|
111
|
+
"""Fixed-width split for OCR bodies — no markdown or code structure here."""
|
|
112
|
+
body = str(text or "").strip()
|
|
113
|
+
if len(body) <= SUMMARY_CHARS:
|
|
114
|
+
return []
|
|
115
|
+
return [
|
|
116
|
+
body[start : start + IMAGE_CHUNK_CHARS]
|
|
117
|
+
for start in range(0, len(body), IMAGE_CHUNK_CHARS)
|
|
118
|
+
]
|
|
@@ -1,160 +1,35 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
What routes here
|
|
9
|
-
----------------
|
|
10
|
-
:func:`detect_modality` reads the MIME type first and the extension second, and
|
|
11
|
-
answers with one of ``text`` / ``image`` / ``audio`` / ``video``. ``video`` is
|
|
12
|
-
deliberately a *recognized but unsupported* answer in this release: keyframe
|
|
13
|
-
extraction needs a decoder this project does not ship, and returning "video,
|
|
14
|
-
out of scope" is worth more than pretending a ``.mov`` is a picture.
|
|
15
|
-
|
|
16
|
-
What an image memory contains
|
|
17
|
-
-----------------------------
|
|
18
|
-
:func:`extract_image_facts` gathers only what it can actually observe:
|
|
19
|
-
|
|
20
|
-
* **dimensions/format** from Pillow (a core dependency);
|
|
21
|
-
* **ocr_text** from ``pytesseract`` when it is installed — otherwise
|
|
22
|
-
``ocr_status="unavailable"`` and no text, never an empty string dressed up as
|
|
23
|
-
a successful read;
|
|
24
|
-
* **caption** from an injected vision-language port, and *only* from there. No
|
|
25
|
-
VLM means ``caption is None``. Composing "Image IMG_2381.png (JPEG 3024x4032)"
|
|
26
|
-
and storing it in the caption field would make metadata indistinguishable
|
|
27
|
-
from a model's description forever after;
|
|
28
|
-
* **embedding** from an injected vision port, which lives in its own vector
|
|
29
|
-
space (see :mod:`latticeai.core.embedding_providers`) and therefore its own
|
|
30
|
-
index — text queries reach images through OCR/caption text, not by scoring a
|
|
31
|
-
BGE vector against CLIP vectors.
|
|
32
|
-
|
|
33
|
-
Brain Core owns none of those models. Every heavy dependency arrives as an
|
|
34
|
-
injected callable (:class:`MultimodalPorts`), which is also why this module
|
|
35
|
-
imports nothing from ``latticeai``.
|
|
1
|
+
"""A picture as a first-class memory: what was observed, and what was written.
|
|
2
|
+
|
|
3
|
+
Two halves, in order: :func:`extract_image_facts` observes one file (size, OCR,
|
|
4
|
+
caption, vector — each with its own status), and :func:`write_image_memory`
|
|
5
|
+
turns those facts into an ``Image`` node with its chunks, concepts and source
|
|
6
|
+
link. Video reuses both verbatim for its keyframes, which is why nothing here
|
|
7
|
+
knows what a video is.
|
|
36
8
|
"""
|
|
37
9
|
|
|
38
10
|
from __future__ import annotations
|
|
39
11
|
|
|
40
12
|
import base64
|
|
41
|
-
import hashlib
|
|
42
13
|
import io
|
|
43
|
-
import
|
|
44
|
-
from dataclasses import dataclass, field
|
|
14
|
+
from dataclasses import dataclass
|
|
45
15
|
from pathlib import Path
|
|
46
16
|
from typing import Any, Callable, Dict, List, Optional
|
|
47
17
|
|
|
48
|
-
from
|
|
49
|
-
from
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
MODALITY_IMAGE
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
18
|
+
from ..quiet import quiet
|
|
19
|
+
from ..utils import utc_now_iso
|
|
20
|
+
from .common import (
|
|
21
|
+
MAX_INDEX_TEXT_CHARS,
|
|
22
|
+
MAX_THUMBNAIL_CHARS,
|
|
23
|
+
MODALITY_IMAGE,
|
|
24
|
+
SUMMARY_CHARS,
|
|
25
|
+
THUMBNAIL_EDGE,
|
|
26
|
+
_sha256_file,
|
|
27
|
+
_sha256_text,
|
|
28
|
+
_split_index_text,
|
|
59
29
|
)
|
|
60
|
-
|
|
61
|
-
# absent: by extension alone they are video, and a voice memo recorded in one
|
|
62
|
-
# of them arrives through an explicit audio MIME type (or through
|
|
63
|
-
# ``VoiceCaptureService``, where the user already said "this is a memo").
|
|
64
|
-
# ``.mid``/``.midi`` are listed for a second reason: CPython's *built-in* mime
|
|
65
|
-
# table has neither, so ``mimetypes`` answers "audio/midi" only on a host that
|
|
66
|
-
# ships a system mime file (macOS reads /etc/apache2/mime.types; a slim Linux
|
|
67
|
-
# container has nothing). Leaving them to the fallback let the platform decide
|
|
68
|
-
# what a MIDI file is — a module table exists precisely so it does not.
|
|
69
|
-
AUDIO_EXTENSIONS = frozenset(
|
|
70
|
-
{".m4a", ".mp3", ".wav", ".aac", ".flac", ".ogg", ".opus", ".mid", ".midi"}
|
|
71
|
-
)
|
|
72
|
-
VIDEO_EXTENSIONS = frozenset({".mp4", ".webm", ".mov", ".mkv", ".avi", ".m4v"})
|
|
30
|
+
from .ports import MultimodalPorts
|
|
73
31
|
|
|
74
|
-
#: Why ``video`` is recognized and still refused — surfaced to the caller.
|
|
75
|
-
VIDEO_OUT_OF_SCOPE = (
|
|
76
|
-
"video ingestion is out of scope in this release (keyframe extraction is "
|
|
77
|
-
"not implemented); the file was not stored"
|
|
78
|
-
)
|
|
79
32
|
|
|
80
|
-
#: Longest OCR/caption body kept on the node (a screenshot is not a novel).
|
|
81
|
-
MAX_INDEX_TEXT_CHARS = 20_000
|
|
82
|
-
#: Summary column budget, matching every other ingest door in the graph.
|
|
83
|
-
SUMMARY_CHARS = 500
|
|
84
|
-
#: Fixed-width chunking for OCR bodies that outgrow the summary.
|
|
85
|
-
IMAGE_CHUNK_CHARS = 900
|
|
86
|
-
#: Longest edge of the stored thumbnail, in pixels.
|
|
87
|
-
THUMBNAIL_EDGE = 96
|
|
88
|
-
#: A thumbnail is a UI affordance, not an archive — drop it past this size.
|
|
89
|
-
MAX_THUMBNAIL_CHARS = 24_000
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def detect_modality(
|
|
93
|
-
path: Optional[str] = None, mime_type: Optional[str] = None
|
|
94
|
-
) -> str:
|
|
95
|
-
"""``text`` | ``image`` | ``audio`` | ``video`` for one candidate file.
|
|
96
|
-
|
|
97
|
-
The declared MIME type wins when it carries a usable top-level type: the
|
|
98
|
-
capture surface saw the bytes, this function only sees a name. Otherwise
|
|
99
|
-
the extension decides, and an unknown extension is ``text`` so existing
|
|
100
|
-
behaviour is untouched.
|
|
101
|
-
"""
|
|
102
|
-
declared = str(mime_type or "").strip().lower().split(";")[0].split("/")[0]
|
|
103
|
-
if declared in {MODALITY_IMAGE, MODALITY_AUDIO, MODALITY_VIDEO}:
|
|
104
|
-
return declared
|
|
105
|
-
# The extension tables come before ``mimetypes`` on purpose: they are where
|
|
106
|
-
# this module's decisions live (``.mp4`` is video unless someone who saw
|
|
107
|
-
# the bytes says otherwise), and the stdlib table varies by platform.
|
|
108
|
-
suffix = Path(str(path or "")).suffix.lower()
|
|
109
|
-
if suffix in IMAGE_EXTENSIONS:
|
|
110
|
-
return MODALITY_IMAGE
|
|
111
|
-
if suffix in AUDIO_EXTENSIONS:
|
|
112
|
-
return MODALITY_AUDIO
|
|
113
|
-
if suffix in VIDEO_EXTENSIONS:
|
|
114
|
-
return MODALITY_VIDEO
|
|
115
|
-
if path:
|
|
116
|
-
guessed, _ = mimetypes.guess_type(str(path))
|
|
117
|
-
top = str(guessed or "").split("/")[0]
|
|
118
|
-
if top in {MODALITY_IMAGE, MODALITY_AUDIO, MODALITY_VIDEO}:
|
|
119
|
-
return top
|
|
120
|
-
return MODALITY_TEXT
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
# ── injected capability ports ────────────────────────────────────────────────
|
|
124
|
-
@dataclass
|
|
125
|
-
class MultimodalPorts:
|
|
126
|
-
"""The optional model-backed capabilities Brain Core cannot ship itself.
|
|
127
|
-
|
|
128
|
-
Every field is a plain callable so the app layer can build it from
|
|
129
|
-
``latticeai.core.embedding_providers`` without Brain Core ever importing
|
|
130
|
-
that package. ``None`` everywhere is the honest default: OCR still runs
|
|
131
|
-
(``pytesseract`` is a local binary, not a model download), and everything
|
|
132
|
-
else reports itself as unavailable.
|
|
133
|
-
"""
|
|
134
|
-
|
|
135
|
-
#: ``(image_path) -> caption or None`` — a loaded VLM, or nothing.
|
|
136
|
-
captioner: Optional[Callable[[str], Optional[str]]] = None
|
|
137
|
-
#: ``(image_path) -> vector`` in the image space (raises when it cannot).
|
|
138
|
-
vision_embedder: Optional[Callable[[str], List[float]]] = None
|
|
139
|
-
#: ``(audio_path) -> transcript`` (raises/returns empty when it cannot).
|
|
140
|
-
transcriber: Optional[Callable[[str], str]] = None
|
|
141
|
-
#: Identity of the vision model, recorded next to every image vector.
|
|
142
|
-
vision_model_id: str = ""
|
|
143
|
-
#: ``image`` (own index + late fusion) or ``shared`` (same space as text).
|
|
144
|
-
vision_space: str = MODALITY_IMAGE
|
|
145
|
-
|
|
146
|
-
def describe(self) -> Dict[str, Any]:
|
|
147
|
-
"""What this install can honestly do with a picture or a recording."""
|
|
148
|
-
return {
|
|
149
|
-
"caption": self.captioner is not None,
|
|
150
|
-
"vision_embedding": self.vision_embedder is not None,
|
|
151
|
-
"transcription": self.transcriber is not None,
|
|
152
|
-
"vision_model_id": self.vision_model_id,
|
|
153
|
-
"vision_space": self.vision_space,
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
# ── image facts ──────────────────────────────────────────────────────────────
|
|
158
33
|
@dataclass
|
|
159
34
|
class ImageFacts:
|
|
160
35
|
"""Everything observed about one image, and the status of each attempt."""
|
|
@@ -369,30 +244,6 @@ def image_quality_score(facts: ImageFacts) -> Dict[str, Any]:
|
|
|
369
244
|
return {"score": round(max(0.0, min(1.0, score)), 4), "reasons": reasons}
|
|
370
245
|
|
|
371
246
|
|
|
372
|
-
# ── graph write ──────────────────────────────────────────────────────────────
|
|
373
|
-
def _sha256_file(path: Path) -> str:
|
|
374
|
-
digest = hashlib.sha256()
|
|
375
|
-
with path.open("rb") as handle:
|
|
376
|
-
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
|
377
|
-
digest.update(block)
|
|
378
|
-
return digest.hexdigest()
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
def _sha256_text(text: str) -> str:
|
|
382
|
-
return hashlib.sha256(str(text).encode("utf-8", "ignore")).hexdigest()
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
def _split_index_text(text: str) -> List[str]:
|
|
386
|
-
"""Fixed-width split for OCR bodies — no markdown or code structure here."""
|
|
387
|
-
body = str(text or "").strip()
|
|
388
|
-
if len(body) <= SUMMARY_CHARS:
|
|
389
|
-
return []
|
|
390
|
-
return [
|
|
391
|
-
body[start : start + IMAGE_CHUNK_CHARS]
|
|
392
|
-
for start in range(0, len(body), IMAGE_CHUNK_CHARS)
|
|
393
|
-
]
|
|
394
|
-
|
|
395
|
-
|
|
396
247
|
def image_node_id(content_hash: str, workspace_id: Optional[str] = None) -> str:
|
|
397
248
|
"""Workspace-scoped, content-addressed id — re-ingesting is idempotent."""
|
|
398
249
|
scoped = f"{workspace_id or 'legacy-global'}|{content_hash}"
|
|
@@ -583,7 +434,7 @@ def _attach_concepts(
|
|
|
583
434
|
body = str(text or "").strip()
|
|
584
435
|
if not body:
|
|
585
436
|
return []
|
|
586
|
-
from
|
|
437
|
+
from ..graph._kg_common import ( # local: keeps this module's import light
|
|
587
438
|
_classify_node_type,
|
|
588
439
|
_extract_concepts,
|
|
589
440
|
)
|
|
@@ -591,7 +442,7 @@ def _attach_concepts(
|
|
|
591
442
|
# The id derivation is imported rather than re-derived: two copies of a
|
|
592
443
|
# node-id rule diverge, and a diverged id is a duplicate concept nobody
|
|
593
444
|
# can see.
|
|
594
|
-
from
|
|
445
|
+
from ..graph.ingest import _scoped_slug_id
|
|
595
446
|
|
|
596
447
|
concept_ids: List[str] = []
|
|
597
448
|
for concept in _extract_concepts(body, limit=10):
|
|
@@ -645,94 +496,3 @@ def _attach_source(
|
|
|
645
496
|
extra={"owner": owner, "workspace_id": workspace_id, "modality": MODALITY_IMAGE},
|
|
646
497
|
)
|
|
647
498
|
)
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
# ── audio ────────────────────────────────────────────────────────────────────
|
|
651
|
-
@dataclass
|
|
652
|
-
class AudioFacts:
|
|
653
|
-
"""A recording, its transcript, and how honestly we got one."""
|
|
654
|
-
|
|
655
|
-
path: str
|
|
656
|
-
#: ``ok`` | ``unavailable`` | ``failed`` | ``supplied``
|
|
657
|
-
transcription_status: str = "unavailable"
|
|
658
|
-
transcript: str = ""
|
|
659
|
-
detail: str = ""
|
|
660
|
-
segments: List[Dict[str, Any]] = field(default_factory=list)
|
|
661
|
-
|
|
662
|
-
@property
|
|
663
|
-
def searchable(self) -> bool:
|
|
664
|
-
return bool(self.transcript)
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
def transcribe_audio(
|
|
668
|
-
path: str,
|
|
669
|
-
*,
|
|
670
|
-
ports: Optional[MultimodalPorts] = None,
|
|
671
|
-
transcript: Optional[str] = None,
|
|
672
|
-
) -> AudioFacts:
|
|
673
|
-
"""Transcribe a recording through the injected port, or say why not.
|
|
674
|
-
|
|
675
|
-
``transcript`` lets a caller that already has text (a phone's own
|
|
676
|
-
dictation, ``VoiceCaptureService``) skip the local model entirely. An
|
|
677
|
-
absent transcriber yields ``transcription_status="unavailable"`` and an
|
|
678
|
-
empty transcript — the recording is still remembered by title and path,
|
|
679
|
-
and the result never claims it is searchable.
|
|
680
|
-
"""
|
|
681
|
-
ports = ports or MultimodalPorts()
|
|
682
|
-
supplied = str(transcript or "").strip()
|
|
683
|
-
if supplied:
|
|
684
|
-
return AudioFacts(path=str(path), transcription_status="supplied", transcript=supplied)
|
|
685
|
-
if ports.transcriber is None:
|
|
686
|
-
return AudioFacts(
|
|
687
|
-
path=str(path),
|
|
688
|
-
transcription_status="unavailable",
|
|
689
|
-
detail="no local transcriber is configured",
|
|
690
|
-
)
|
|
691
|
-
try:
|
|
692
|
-
text = str(ports.transcriber(str(path)) or "").strip()
|
|
693
|
-
except Exception as exc: # noqa: BLE001 — a broken transcriber is a state
|
|
694
|
-
return AudioFacts(path=str(path), transcription_status="failed", detail=str(exc))
|
|
695
|
-
if not text:
|
|
696
|
-
return AudioFacts(
|
|
697
|
-
path=str(path),
|
|
698
|
-
transcription_status="failed",
|
|
699
|
-
detail="the transcriber returned no text",
|
|
700
|
-
)
|
|
701
|
-
return AudioFacts(path=str(path), transcription_status="ok", transcript=text)
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
def audio_quality_score(facts: AudioFacts) -> Dict[str, Any]:
|
|
705
|
-
"""How much of this recording is actually retrievable later."""
|
|
706
|
-
if not facts.transcript:
|
|
707
|
-
return {"score": 0.0, "reasons": ["no_transcript"]}
|
|
708
|
-
# A transcript is text: length is the only honest extra signal here, and
|
|
709
|
-
# the text pipeline scores the wording itself downstream.
|
|
710
|
-
score = 0.5 + 0.5 * min(1.0, len(facts.transcript) / 400.0)
|
|
711
|
-
return {"score": round(score, 4), "reasons": ["transcript"]}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
__all__ = [
|
|
715
|
-
"AUDIO_EXTENSIONS",
|
|
716
|
-
"IMAGE_CHUNK_CHARS",
|
|
717
|
-
"IMAGE_EXTENSIONS",
|
|
718
|
-
"MAX_INDEX_TEXT_CHARS",
|
|
719
|
-
"MAX_THUMBNAIL_CHARS",
|
|
720
|
-
"MODALITY_AUDIO",
|
|
721
|
-
"MODALITY_IMAGE",
|
|
722
|
-
"MODALITY_TEXT",
|
|
723
|
-
"MODALITY_VIDEO",
|
|
724
|
-
"SUMMARY_CHARS",
|
|
725
|
-
"THUMBNAIL_EDGE",
|
|
726
|
-
"VIDEO_EXTENSIONS",
|
|
727
|
-
"VIDEO_OUT_OF_SCOPE",
|
|
728
|
-
"AudioFacts",
|
|
729
|
-
"ImageFacts",
|
|
730
|
-
"MultimodalPorts",
|
|
731
|
-
"audio_quality_score",
|
|
732
|
-
"detect_modality",
|
|
733
|
-
"extract_image_facts",
|
|
734
|
-
"image_node_id",
|
|
735
|
-
"image_quality_score",
|
|
736
|
-
"transcribe_audio",
|
|
737
|
-
"write_image_memory",
|
|
738
|
-
]
|