You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/10/05 17:04:50 UTC

(camel) branch main updated: CAMEL-25027: camel-langchain4j-ingest - Add media modality (#27289)

This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 78e9ccbd27b4 CAMEL-25027: camel-langchain4j-ingest - Add media modality (#27289)
78e9ccbd27b4 is described below

commit 78e9ccbd27b467311de1a5f91c4fc5df98d32f6a
Author: Jiří Ondrušek <on...@gmail.com>
AuthorDate: Mon Oct 5 19:04:43 2026 +0200

    CAMEL-25027: camel-langchain4j-ingest - Add media modality (#27289)
    
    Co-Authored-By: Claude Fable 5.1 <no...@anthropic.com>
---
 .../catalog/components/langchain4j-ingest.json     |  62 ++--
 .../catalog/docs/langchain4j-ingest-component.adoc |  50 +++
 .../LangChain4jIngestComponentConfigurer.java      |   9 +
 .../LangChain4jIngestConfigurationConfigurer.java  |   9 +
 .../LangChain4jIngestEndpointConfigurer.java       |   9 +
 .../LangChain4jIngestEndpointUriFactory.java       |   4 +-
 .../langchain4j/ingest/langchain4j-ingest.json     |  62 ++--
 .../main/docs/langchain4j-ingest-component.adoc    |  50 +++
 .../langchain4j/ingest/IngestModality.java         |  33 ++
 .../langchain4j/ingest/IngestService.java          |   4 -
 .../langchain4j/ingest/LangChain4jIngest.java      |   6 +
 .../ingest/LangChain4jIngestConfiguration.java     |  65 +++-
 .../ingest/LangChain4jIngestProducer.java          | 180 +++++++++-
 .../langchain4j/ingest/MediaIngestService.java     | 106 ++++++
 .../component/langchain4j/ingest/MediaTypes.java   |  86 +++++
 .../ingest/DeterministicEmbeddingModel.java        |  17 +-
 .../ingest/DeterministicMediaEmbeddingModel.java   | 125 +++++++
 .../ingest/LangChain4jIngestMediaFolderTest.java   | 146 ++++++++
 .../ingest/LangChain4jIngestMediaTest.java         | 383 +++++++++++++++++++++
 .../ingest/LangChain4jIngestValidationTest.java    |  49 +++
 .../Langchain4jIngestComponentBuilderFactory.java  |  84 ++++-
 .../LangChain4jIngestEndpointBuilderFactory.java   | 133 +++++--
 22 files changed, 1537 insertions(+), 135 deletions(-)

diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-ingest.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-ingest.json
index 6f6c6f5fbeb7..2af22e058109 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-ingest.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-ingest.json
@@ -25,40 +25,44 @@
   },
   "componentProperties": {
     "configuration": { "index": 0, "kind": "property", "displayName": "Configuration", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "deprecated": false, "autowired": false, "secret": false, "description": "The configuration." },
-    "documentIdHeader": { "index": 1, "kind": "property", "displayName": "Document Id Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "CamelLangChain4jIngestDocumentId", "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Name of the header carrying [...]
-    "embeddingBatchSize": { "index": 2, "kind": "property", "displayName": "Embedding Batch Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 32, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How many segments are embedded per request to the embedding model.  [...]
-    "embeddingModel": { "index": 3, "kind": "property", "displayName": "Embedding Model", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.model.embedding.EmbeddingModel", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The EmbeddingModel to embed segm [...]
-    "embeddingStore": { "index": 4, "kind": "property", "displayName": "Embedding Store", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.store.embedding.EmbeddingStore<dev.langchain4j.data.segment.TextSegment>", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "descr [...]
-    "lazyStartProducer": { "index": 5, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail [...]
-    "maxOverlapSize": { "index": 6, "kind": "property", "displayName": "Max Overlap Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 50, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How much of the previous segment each segment repeats, in characters. Overl [...]
-    "maxSegmentSize": { "index": 7, "kind": "property", "displayName": "Max Segment Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 500, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one segment, in characters." },
-    "autowiredEnabled": { "index": 8, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching t [...]
-    "documentSplitter": { "index": 9, "kind": "property", "displayName": "Document Splitter", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "dev.langchain4j.data.document.DocumentSplitter", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The DocumentSplitter deciding how a docume [...]
-    "idempotentRepository": { "index": 10, "kind": "property", "displayName": "Idempotent Repository", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.IdempotentRepository", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The IdempotentRepository remembering a [...]
-    "maxDocumentSize": { "index": 11, "kind": "property", "displayName": "Max Document Size", "group": "advanced", "label": "advanced", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one document in characters, applied to the text a [...]
-    "documentFilter": { "index": 12, "kind": "property", "displayName": "Document Filter", "group": "filter", "label": "filter", "required": false, "type": "object", "javaType": "org.apache.camel.Predicate", "deprecated": false, "autowired": false, "secret": false, "supportSimpleExpression": true, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "A Predicate deciding whether a deliv [...]
-    "excludeId": { "index": 13, "kind": "property", "displayName": "Exclude Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns for document ids to skip. Exclusion wins over  [...]
-    "includeId": { "index": 14, "kind": "property", "displayName": "Include Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns the document id must match to be ingested. A n [...]
-    "minDocumentSize": { "index": 15, "kind": "property", "displayName": "Min Document Size", "group": "filter", "label": "filter", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Minimum size of one document in characters; 0, the default, means no  [...]
+    "contentType": { "index": 1, "kind": "property", "displayName": "Content Type", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "MIME type of a media body, such as audio\/wav or image\/png, handed to the embedding model [...]
+    "documentIdHeader": { "index": 2, "kind": "property", "displayName": "Document Id Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "CamelLangChain4jIngestDocumentId", "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Name of the header carrying [...]
+    "embeddingBatchSize": { "index": 3, "kind": "property", "displayName": "Embedding Batch Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 32, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How many segments are embedded per request to the embedding model.  [...]
+    "embeddingModel": { "index": 4, "kind": "property", "displayName": "Embedding Model", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.model.embedding.EmbeddingModel", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The EmbeddingModel to embed segm [...]
+    "embeddingStore": { "index": 5, "kind": "property", "displayName": "Embedding Store", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.store.embedding.EmbeddingStore<dev.langchain4j.data.segment.TextSegment>", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "descr [...]
+    "lazyStartProducer": { "index": 6, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail [...]
+    "maxOverlapSize": { "index": 7, "kind": "property", "displayName": "Max Overlap Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 50, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How much of the previous segment each segment repeats, in characters. Overl [...]
+    "maxSegmentSize": { "index": 8, "kind": "property", "displayName": "Max Segment Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 500, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one segment, in characters." },
+    "modality": { "index": 9, "kind": "property", "displayName": "Modality", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.langchain4j.ingest.IngestModality", "enum": [ "TEXT", "MEDIA" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "TEXT", "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Wh [...]
+    "autowiredEnabled": { "index": 10, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching  [...]
+    "documentSplitter": { "index": 11, "kind": "property", "displayName": "Document Splitter", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "dev.langchain4j.data.document.DocumentSplitter", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The DocumentSplitter deciding how a docum [...]
+    "idempotentRepository": { "index": 12, "kind": "property", "displayName": "Idempotent Repository", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.IdempotentRepository", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The IdempotentRepository remembering a [...]
+    "maxDocumentSize": { "index": 13, "kind": "property", "displayName": "Max Document Size", "group": "advanced", "label": "advanced", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one document: characters of the text about to be  [...]
+    "documentFilter": { "index": 14, "kind": "property", "displayName": "Document Filter", "group": "filter", "label": "filter", "required": false, "type": "object", "javaType": "org.apache.camel.Predicate", "deprecated": false, "autowired": false, "secret": false, "supportSimpleExpression": true, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "A Predicate deciding whether a deliv [...]
+    "excludeId": { "index": 15, "kind": "property", "displayName": "Exclude Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns for document ids to skip. Exclusion wins over  [...]
+    "includeId": { "index": 16, "kind": "property", "displayName": "Include Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns the document id must match to be ingested. A n [...]
+    "minDocumentSize": { "index": 17, "kind": "property", "displayName": "Min Document Size", "group": "filter", "label": "filter", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Minimum size of one document in characters (bytes with modality=media [...]
   },
   "headers": {
     "CamelLangChain4jIngestDocumentId": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The stable document id of the ingested payload, read when the documentIdHeader endpoint option does not name another header. The CamelLangChain4jIngestDocumentId exchange property, when set, takes precedence over any header.", "c [...]
   },
   "properties": {
     "pipelineName": { "index": 0, "kind": "path", "displayName": "Pipeline Name", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The pipeline name, stamped on every written segment as the camel_ingest_pipeline metadata and used in error messages" },
-    "documentIdHeader": { "index": 1, "kind": "parameter", "displayName": "Document Id Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "CamelLangChain4jIngestDocumentId", "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Name of the header carryin [...]
-    "embeddingBatchSize": { "index": 2, "kind": "parameter", "displayName": "Embedding Batch Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 32, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How many segments are embedded per request to the embedding model. [...]
-    "embeddingModel": { "index": 3, "kind": "parameter", "displayName": "Embedding Model", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.model.embedding.EmbeddingModel", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The EmbeddingModel to embed seg [...]
-    "embeddingStore": { "index": 4, "kind": "parameter", "displayName": "Embedding Store", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.store.embedding.EmbeddingStore<dev.langchain4j.data.segment.TextSegment>", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "desc [...]
-    "maxOverlapSize": { "index": 5, "kind": "parameter", "displayName": "Max Overlap Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 50, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How much of the previous segment each segment repeats, in characters. Over [...]
-    "maxSegmentSize": { "index": 6, "kind": "parameter", "displayName": "Max Segment Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 500, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one segment, in characters." },
-    "lazyStartProducer": { "index": 7, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...]
-    "documentSplitter": { "index": 8, "kind": "parameter", "displayName": "Document Splitter", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "dev.langchain4j.data.document.DocumentSplitter", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The DocumentSplitter deciding how a docum [...]
-    "idempotentRepository": { "index": 9, "kind": "parameter", "displayName": "Idempotent Repository", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.IdempotentRepository", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The IdempotentRepository remembering a [...]
-    "maxDocumentSize": { "index": 10, "kind": "parameter", "displayName": "Max Document Size", "group": "advanced", "label": "advanced", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one document in characters, applied to the text  [...]
-    "documentFilter": { "index": 11, "kind": "parameter", "displayName": "Document Filter", "group": "filter", "label": "filter", "required": false, "type": "object", "javaType": "org.apache.camel.Predicate", "deprecated": false, "autowired": false, "secret": false, "supportSimpleExpression": true, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "A Predicate deciding whether a deli [...]
-    "excludeId": { "index": 12, "kind": "parameter", "displayName": "Exclude Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns for document ids to skip. Exclusion wins over [...]
-    "includeId": { "index": 13, "kind": "parameter", "displayName": "Include Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns the document id must match to be ingested. A  [...]
-    "minDocumentSize": { "index": 14, "kind": "parameter", "displayName": "Min Document Size", "group": "filter", "label": "filter", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Minimum size of one document in characters; 0, the default, means no [...]
+    "contentType": { "index": 1, "kind": "parameter", "displayName": "Content Type", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "MIME type of a media body, such as audio\/wav or image\/png, handed to the embedding mode [...]
+    "documentIdHeader": { "index": 2, "kind": "parameter", "displayName": "Document Id Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "CamelLangChain4jIngestDocumentId", "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Name of the header carryin [...]
+    "embeddingBatchSize": { "index": 3, "kind": "parameter", "displayName": "Embedding Batch Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 32, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How many segments are embedded per request to the embedding model. [...]
+    "embeddingModel": { "index": 4, "kind": "parameter", "displayName": "Embedding Model", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.model.embedding.EmbeddingModel", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The EmbeddingModel to embed seg [...]
+    "embeddingStore": { "index": 5, "kind": "parameter", "displayName": "Embedding Store", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.store.embedding.EmbeddingStore<dev.langchain4j.data.segment.TextSegment>", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "desc [...]
+    "maxOverlapSize": { "index": 6, "kind": "parameter", "displayName": "Max Overlap Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 50, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How much of the previous segment each segment repeats, in characters. Over [...]
+    "maxSegmentSize": { "index": 7, "kind": "parameter", "displayName": "Max Segment Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 500, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one segment, in characters." },
+    "modality": { "index": 8, "kind": "parameter", "displayName": "Modality", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.langchain4j.ingest.IngestModality", "enum": [ "TEXT", "MEDIA" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "TEXT", "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "W [...]
+    "lazyStartProducer": { "index": 9, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...]
+    "documentSplitter": { "index": 10, "kind": "parameter", "displayName": "Document Splitter", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "dev.langchain4j.data.document.DocumentSplitter", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The DocumentSplitter deciding how a docu [...]
+    "idempotentRepository": { "index": 11, "kind": "parameter", "displayName": "Idempotent Repository", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.IdempotentRepository", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The IdempotentRepository remembering  [...]
+    "maxDocumentSize": { "index": 12, "kind": "parameter", "displayName": "Max Document Size", "group": "advanced", "label": "advanced", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one document: characters of the text about to be [...]
+    "documentFilter": { "index": 13, "kind": "parameter", "displayName": "Document Filter", "group": "filter", "label": "filter", "required": false, "type": "object", "javaType": "org.apache.camel.Predicate", "deprecated": false, "autowired": false, "secret": false, "supportSimpleExpression": true, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "A Predicate deciding whether a deli [...]
+    "excludeId": { "index": 14, "kind": "parameter", "displayName": "Exclude Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns for document ids to skip. Exclusion wins over [...]
+    "includeId": { "index": 15, "kind": "parameter", "displayName": "Include Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns the document id must match to be ingested. A  [...]
+    "minDocumentSize": { "index": 16, "kind": "parameter", "displayName": "Min Document Size", "group": "filter", "label": "filter", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Minimum size of one document in characters (bytes with modality=medi [...]
   }
 }
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-ingest-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-ingest-component.adoc
index df4d759f9ecb..114985860897 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-ingest-component.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-ingest-component.adoc
@@ -201,6 +201,56 @@ memory- and CPU-expensive and a small file can still parse into a lot of text. A
 document fails the exchange like any other error (see below) and releases its dedup claim, so
 a trimmed re-delivery under the same id still ingests.
 
+=== Ingesting media: images, audio, video and PDF
+
+With `modality=media` the body is read as bytes and embedded whole, as one vector, by a
+multimodal embedding model. The model must list the medium (`IMAGE`, `AUDIO`, `VIDEO` or
+`PDF`) in its `supportedContentTypes()`; a text-only model fails the start. The vector is
+stored under a placeholder segment whose text is the document id, with the same identity
+metadata as text segments plus the MIME type under `camel_ingest_content_type`, so
+retrieval and citation work unchanged and a retriever on a shared store can tell a media
+hit from text.
+
+[source,java]
+----
+from("file:/var/data/photos?noop=true&recursive=true&readLock=changed&antInclude=**/*.jpg,**/*.png")
+    .to("langchain4j-ingest:photos?modality=media&documentIdHeader=CamelFileName");
+----
+
+Leave `charset` unset on the consumer: a file consumer with one converts the file through
+a Reader, which corrupts media. Keep other files out of the folder with the consumer's
+`antInclude`, as above, or the endpoint's `includeId` and `excludeId`: an unsupported file
+fails the exchange and is retried on every poll.
+
+The medium is taken from the MIME type: the `contentType` option when set, otherwise the
+document id's file extension looked up in Camel's MIME table (`MimeTypeHelper` in
+camel-util: wav, mp3, png, mp4, pdf and the like). Message headers are never consulted. An
+id ending in a query string, a URL say, has no usable extension and needs `contentType`. A
+document whose type is unknown, is not a medium or is one the model does not support fails
+the exchange before its dedup claim and before its body is read, so a mixed folder fails
+per file. A `.webm` file counts as video; a browser audio recording needs
+`contentType=audio/webm`. The family decides the medium and the provider decides the
+formats within it, so an `image/svg+xml` file passes as an image and fails at the
+provider. A `contentType` the model does not support, or one set with `modality=text`,
+fails the start.
+
+Nothing is split: `maxSegmentSize`, `maxOverlapSize` and `embeddingBatchSize` do not
+apply, a configured `documentSplitter` fails the start, and `maxDocumentSize` and
+`minDocumentSize` count bytes. Base64 encoding adds a third, so set `maxDocumentSize`
+against the model's request limit. A file consumer announces the length in
+`CamelFileLength`, which is checked against the cap before the file is read; any other
+body is checked once it is in memory. A `documentFilter` runs before the body is read and
+sees it as delivered, a file or stream rather than text, so filter on headers or the id.
+
+Several LangChain4j providers embed images (Jina, Voyage AI, Cohere, Google Gemini and
+Bedrock Titan among them); none embeds audio yet, so an audio model is typically a custom
+`EmbeddingModel` calling a service such as Wav2Vec2 or CLAP. The same bean embeds the
+query at retrieval time. A model with an `EmbeddingModelListener` attached is refused at
+start: LangChain4j 1.20 builds the listener context from the input text, which media lacks.
+Only a listener added by hand is affected; the Quarkus and Spring Boot LangChain4j
+integrations attach none to embedding models today. Media embedding goes through
+LangChain4j's `EmbeddingRequest` API, still experimental in 1.20, so it may change.
+
 === When ingestion fails
 
 A failure while splitting, embedding or storing — a rate-limited model, an unreachable store —
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestComponentConfigurer.java b/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestComponentConfigurer.java
index 300e1661b56e..c65f8fb0c909 100644
--- a/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestComponentConfigurer.java
+++ b/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestComponentConfigurer.java
@@ -33,6 +33,8 @@ public class LangChain4jIngestComponentConfigurer extends PropertyConfigurerSupp
         case "autowiredenabled":
         case "autowiredEnabled": target.setAutowiredEnabled(property(camelContext, boolean.class, value)); return true;
         case "configuration": target.setConfiguration(property(camelContext, org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration.class, value)); return true;
+        case "contenttype":
+        case "contentType": getOrCreateConfiguration(target).setContentType(property(camelContext, java.lang.String.class, value)); return true;
         case "documentfilter":
         case "documentFilter": getOrCreateConfiguration(target).setDocumentFilter(property(camelContext, org.apache.camel.Predicate.class, value)); return true;
         case "documentidheader":
@@ -61,6 +63,7 @@ public class LangChain4jIngestComponentConfigurer extends PropertyConfigurerSupp
         case "maxSegmentSize": getOrCreateConfiguration(target).setMaxSegmentSize(property(camelContext, int.class, value)); return true;
         case "mindocumentsize":
         case "minDocumentSize": getOrCreateConfiguration(target).setMinDocumentSize(property(camelContext, int.class, value)); return true;
+        case "modality": getOrCreateConfiguration(target).setModality(property(camelContext, org.apache.camel.component.langchain4j.ingest.IngestModality.class, value)); return true;
         default: return false;
         }
     }
@@ -76,6 +79,8 @@ public class LangChain4jIngestComponentConfigurer extends PropertyConfigurerSupp
         case "autowiredenabled":
         case "autowiredEnabled": return boolean.class;
         case "configuration": return org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration.class;
+        case "contenttype":
+        case "contentType": return java.lang.String.class;
         case "documentfilter":
         case "documentFilter": return org.apache.camel.Predicate.class;
         case "documentidheader":
@@ -104,6 +109,7 @@ public class LangChain4jIngestComponentConfigurer extends PropertyConfigurerSupp
         case "maxSegmentSize": return int.class;
         case "mindocumentsize":
         case "minDocumentSize": return int.class;
+        case "modality": return org.apache.camel.component.langchain4j.ingest.IngestModality.class;
         default: return null;
         }
     }
@@ -115,6 +121,8 @@ public class LangChain4jIngestComponentConfigurer extends PropertyConfigurerSupp
         case "autowiredenabled":
         case "autowiredEnabled": return target.isAutowiredEnabled();
         case "configuration": return target.getConfiguration();
+        case "contenttype":
+        case "contentType": return getOrCreateConfiguration(target).getContentType();
         case "documentfilter":
         case "documentFilter": return getOrCreateConfiguration(target).getDocumentFilter();
         case "documentidheader":
@@ -143,6 +151,7 @@ public class LangChain4jIngestComponentConfigurer extends PropertyConfigurerSupp
         case "maxSegmentSize": return getOrCreateConfiguration(target).getMaxSegmentSize();
         case "mindocumentsize":
         case "minDocumentSize": return getOrCreateConfiguration(target).getMinDocumentSize();
+        case "modality": return getOrCreateConfiguration(target).getModality();
         default: return null;
         }
     }
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestConfigurationConfigurer.java b/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestConfigurationConfigurer.java
index 5e62f6cc245f..98c7a84cafc9 100644
--- a/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestConfigurationConfigurer.java
+++ b/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestConfigurationConfigurer.java
@@ -23,6 +23,8 @@ public class LangChain4jIngestConfigurationConfigurer extends org.apache.camel.s
     public boolean configure(CamelContext camelContext, Object obj, String name, Object value, boolean ignoreCase) {
         org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration target = (org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration) obj;
         switch (ignoreCase ? name.toLowerCase() : name) {
+        case "contenttype":
+        case "contentType": target.setContentType(property(camelContext, java.lang.String.class, value)); return true;
         case "documentfilter":
         case "documentFilter": target.setDocumentFilter(property(camelContext, org.apache.camel.Predicate.class, value)); return true;
         case "documentidheader":
@@ -49,6 +51,7 @@ public class LangChain4jIngestConfigurationConfigurer extends org.apache.camel.s
         case "maxSegmentSize": target.setMaxSegmentSize(property(camelContext, int.class, value)); return true;
         case "mindocumentsize":
         case "minDocumentSize": target.setMinDocumentSize(property(camelContext, int.class, value)); return true;
+        case "modality": target.setModality(property(camelContext, org.apache.camel.component.langchain4j.ingest.IngestModality.class, value)); return true;
         default: return false;
         }
     }
@@ -56,6 +59,8 @@ public class LangChain4jIngestConfigurationConfigurer extends org.apache.camel.s
     @Override
     public Class<?> getOptionType(String name, boolean ignoreCase) {
         switch (ignoreCase ? name.toLowerCase() : name) {
+        case "contenttype":
+        case "contentType": return java.lang.String.class;
         case "documentfilter":
         case "documentFilter": return org.apache.camel.Predicate.class;
         case "documentidheader":
@@ -82,6 +87,7 @@ public class LangChain4jIngestConfigurationConfigurer extends org.apache.camel.s
         case "maxSegmentSize": return int.class;
         case "mindocumentsize":
         case "minDocumentSize": return int.class;
+        case "modality": return org.apache.camel.component.langchain4j.ingest.IngestModality.class;
         default: return null;
         }
     }
@@ -90,6 +96,8 @@ public class LangChain4jIngestConfigurationConfigurer extends org.apache.camel.s
     public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
         org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration target = (org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration) obj;
         switch (ignoreCase ? name.toLowerCase() : name) {
+        case "contenttype":
+        case "contentType": return target.getContentType();
         case "documentfilter":
         case "documentFilter": return target.getDocumentFilter();
         case "documentidheader":
@@ -116,6 +124,7 @@ public class LangChain4jIngestConfigurationConfigurer extends org.apache.camel.s
         case "maxSegmentSize": return target.getMaxSegmentSize();
         case "mindocumentsize":
         case "minDocumentSize": return target.getMinDocumentSize();
+        case "modality": return target.getModality();
         default: return null;
         }
     }
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestEndpointConfigurer.java b/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestEndpointConfigurer.java
index 5c2f4ce5dcba..d62ac9945fa4 100644
--- a/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestEndpointConfigurer.java
+++ b/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestEndpointConfigurer.java
@@ -23,6 +23,8 @@ public class LangChain4jIngestEndpointConfigurer extends PropertyConfigurerSuppo
     public boolean configure(CamelContext camelContext, Object obj, String name, Object value, boolean ignoreCase) {
         LangChain4jIngestEndpoint target = (LangChain4jIngestEndpoint) obj;
         switch (ignoreCase ? name.toLowerCase() : name) {
+        case "contenttype":
+        case "contentType": target.getConfiguration().setContentType(property(camelContext, java.lang.String.class, value)); return true;
         case "documentfilter":
         case "documentFilter": target.getConfiguration().setDocumentFilter(property(camelContext, org.apache.camel.Predicate.class, value)); return true;
         case "documentidheader":
@@ -51,6 +53,7 @@ public class LangChain4jIngestEndpointConfigurer extends PropertyConfigurerSuppo
         case "maxSegmentSize": target.getConfiguration().setMaxSegmentSize(property(camelContext, int.class, value)); return true;
         case "mindocumentsize":
         case "minDocumentSize": target.getConfiguration().setMinDocumentSize(property(camelContext, int.class, value)); return true;
+        case "modality": target.getConfiguration().setModality(property(camelContext, org.apache.camel.component.langchain4j.ingest.IngestModality.class, value)); return true;
         default: return false;
         }
     }
@@ -63,6 +66,8 @@ public class LangChain4jIngestEndpointConfigurer extends PropertyConfigurerSuppo
     @Override
     public Class<?> getOptionType(String name, boolean ignoreCase) {
         switch (ignoreCase ? name.toLowerCase() : name) {
+        case "contenttype":
+        case "contentType": return java.lang.String.class;
         case "documentfilter":
         case "documentFilter": return org.apache.camel.Predicate.class;
         case "documentidheader":
@@ -91,6 +96,7 @@ public class LangChain4jIngestEndpointConfigurer extends PropertyConfigurerSuppo
         case "maxSegmentSize": return int.class;
         case "mindocumentsize":
         case "minDocumentSize": return int.class;
+        case "modality": return org.apache.camel.component.langchain4j.ingest.IngestModality.class;
         default: return null;
         }
     }
@@ -99,6 +105,8 @@ public class LangChain4jIngestEndpointConfigurer extends PropertyConfigurerSuppo
     public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
         LangChain4jIngestEndpoint target = (LangChain4jIngestEndpoint) obj;
         switch (ignoreCase ? name.toLowerCase() : name) {
+        case "contenttype":
+        case "contentType": return target.getConfiguration().getContentType();
         case "documentfilter":
         case "documentFilter": return target.getConfiguration().getDocumentFilter();
         case "documentidheader":
@@ -127,6 +135,7 @@ public class LangChain4jIngestEndpointConfigurer extends PropertyConfigurerSuppo
         case "maxSegmentSize": return target.getConfiguration().getMaxSegmentSize();
         case "mindocumentsize":
         case "minDocumentSize": return target.getConfiguration().getMinDocumentSize();
+        case "modality": return target.getConfiguration().getModality();
         default: return null;
         }
     }
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestEndpointUriFactory.java b/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestEndpointUriFactory.java
index 3d7678324afd..b18175a20c16 100644
--- a/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestEndpointUriFactory.java
+++ b/components/camel-ai/camel-langchain4j-ingest/src/generated/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestEndpointUriFactory.java
@@ -24,7 +24,8 @@ public class LangChain4jIngestEndpointUriFactory extends org.apache.camel.suppor
     private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
     private static final Map<String, String> MULTI_VALUE_PREFIXES;
     static {
-        Set<String> props = new HashSet<>(15);
+        Set<String> props = new HashSet<>(17);
+        props.add("contentType");
         props.add("documentFilter");
         props.add("documentIdHeader");
         props.add("documentSplitter");
@@ -39,6 +40,7 @@ public class LangChain4jIngestEndpointUriFactory extends org.apache.camel.suppor
         props.add("maxOverlapSize");
         props.add("maxSegmentSize");
         props.add("minDocumentSize");
+        props.add("modality");
         props.add("pipelineName");
         PROPERTY_NAMES = Collections.unmodifiableSet(props);
         SECRET_PROPERTY_NAMES = Collections.emptySet();
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/ingest/langchain4j-ingest.json b/components/camel-ai/camel-langchain4j-ingest/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/ingest/langchain4j-ingest.json
index 6f6c6f5fbeb7..2af22e058109 100644
--- a/components/camel-ai/camel-langchain4j-ingest/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/ingest/langchain4j-ingest.json
+++ b/components/camel-ai/camel-langchain4j-ingest/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/ingest/langchain4j-ingest.json
@@ -25,40 +25,44 @@
   },
   "componentProperties": {
     "configuration": { "index": 0, "kind": "property", "displayName": "Configuration", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "deprecated": false, "autowired": false, "secret": false, "description": "The configuration." },
-    "documentIdHeader": { "index": 1, "kind": "property", "displayName": "Document Id Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "CamelLangChain4jIngestDocumentId", "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Name of the header carrying [...]
-    "embeddingBatchSize": { "index": 2, "kind": "property", "displayName": "Embedding Batch Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 32, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How many segments are embedded per request to the embedding model.  [...]
-    "embeddingModel": { "index": 3, "kind": "property", "displayName": "Embedding Model", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.model.embedding.EmbeddingModel", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The EmbeddingModel to embed segm [...]
-    "embeddingStore": { "index": 4, "kind": "property", "displayName": "Embedding Store", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.store.embedding.EmbeddingStore<dev.langchain4j.data.segment.TextSegment>", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "descr [...]
-    "lazyStartProducer": { "index": 5, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail [...]
-    "maxOverlapSize": { "index": 6, "kind": "property", "displayName": "Max Overlap Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 50, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How much of the previous segment each segment repeats, in characters. Overl [...]
-    "maxSegmentSize": { "index": 7, "kind": "property", "displayName": "Max Segment Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 500, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one segment, in characters." },
-    "autowiredEnabled": { "index": 8, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching t [...]
-    "documentSplitter": { "index": 9, "kind": "property", "displayName": "Document Splitter", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "dev.langchain4j.data.document.DocumentSplitter", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The DocumentSplitter deciding how a docume [...]
-    "idempotentRepository": { "index": 10, "kind": "property", "displayName": "Idempotent Repository", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.IdempotentRepository", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The IdempotentRepository remembering a [...]
-    "maxDocumentSize": { "index": 11, "kind": "property", "displayName": "Max Document Size", "group": "advanced", "label": "advanced", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one document in characters, applied to the text a [...]
-    "documentFilter": { "index": 12, "kind": "property", "displayName": "Document Filter", "group": "filter", "label": "filter", "required": false, "type": "object", "javaType": "org.apache.camel.Predicate", "deprecated": false, "autowired": false, "secret": false, "supportSimpleExpression": true, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "A Predicate deciding whether a deliv [...]
-    "excludeId": { "index": 13, "kind": "property", "displayName": "Exclude Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns for document ids to skip. Exclusion wins over  [...]
-    "includeId": { "index": 14, "kind": "property", "displayName": "Include Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns the document id must match to be ingested. A n [...]
-    "minDocumentSize": { "index": 15, "kind": "property", "displayName": "Min Document Size", "group": "filter", "label": "filter", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Minimum size of one document in characters; 0, the default, means no  [...]
+    "contentType": { "index": 1, "kind": "property", "displayName": "Content Type", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "MIME type of a media body, such as audio\/wav or image\/png, handed to the embedding model [...]
+    "documentIdHeader": { "index": 2, "kind": "property", "displayName": "Document Id Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "CamelLangChain4jIngestDocumentId", "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Name of the header carrying [...]
+    "embeddingBatchSize": { "index": 3, "kind": "property", "displayName": "Embedding Batch Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 32, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How many segments are embedded per request to the embedding model.  [...]
+    "embeddingModel": { "index": 4, "kind": "property", "displayName": "Embedding Model", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.model.embedding.EmbeddingModel", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The EmbeddingModel to embed segm [...]
+    "embeddingStore": { "index": 5, "kind": "property", "displayName": "Embedding Store", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.store.embedding.EmbeddingStore<dev.langchain4j.data.segment.TextSegment>", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "descr [...]
+    "lazyStartProducer": { "index": 6, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail [...]
+    "maxOverlapSize": { "index": 7, "kind": "property", "displayName": "Max Overlap Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 50, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How much of the previous segment each segment repeats, in characters. Overl [...]
+    "maxSegmentSize": { "index": 8, "kind": "property", "displayName": "Max Segment Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 500, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one segment, in characters." },
+    "modality": { "index": 9, "kind": "property", "displayName": "Modality", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.langchain4j.ingest.IngestModality", "enum": [ "TEXT", "MEDIA" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "TEXT", "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Wh [...]
+    "autowiredEnabled": { "index": 10, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching  [...]
+    "documentSplitter": { "index": 11, "kind": "property", "displayName": "Document Splitter", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "dev.langchain4j.data.document.DocumentSplitter", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The DocumentSplitter deciding how a docum [...]
+    "idempotentRepository": { "index": 12, "kind": "property", "displayName": "Idempotent Repository", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.IdempotentRepository", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The IdempotentRepository remembering a [...]
+    "maxDocumentSize": { "index": 13, "kind": "property", "displayName": "Max Document Size", "group": "advanced", "label": "advanced", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one document: characters of the text about to be  [...]
+    "documentFilter": { "index": 14, "kind": "property", "displayName": "Document Filter", "group": "filter", "label": "filter", "required": false, "type": "object", "javaType": "org.apache.camel.Predicate", "deprecated": false, "autowired": false, "secret": false, "supportSimpleExpression": true, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "A Predicate deciding whether a deliv [...]
+    "excludeId": { "index": 15, "kind": "property", "displayName": "Exclude Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns for document ids to skip. Exclusion wins over  [...]
+    "includeId": { "index": 16, "kind": "property", "displayName": "Include Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns the document id must match to be ingested. A n [...]
+    "minDocumentSize": { "index": 17, "kind": "property", "displayName": "Min Document Size", "group": "filter", "label": "filter", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Minimum size of one document in characters (bytes with modality=media [...]
   },
   "headers": {
     "CamelLangChain4jIngestDocumentId": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The stable document id of the ingested payload, read when the documentIdHeader endpoint option does not name another header. The CamelLangChain4jIngestDocumentId exchange property, when set, takes precedence over any header.", "c [...]
   },
   "properties": {
     "pipelineName": { "index": 0, "kind": "path", "displayName": "Pipeline Name", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The pipeline name, stamped on every written segment as the camel_ingest_pipeline metadata and used in error messages" },
-    "documentIdHeader": { "index": 1, "kind": "parameter", "displayName": "Document Id Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "CamelLangChain4jIngestDocumentId", "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Name of the header carryin [...]
-    "embeddingBatchSize": { "index": 2, "kind": "parameter", "displayName": "Embedding Batch Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 32, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How many segments are embedded per request to the embedding model. [...]
-    "embeddingModel": { "index": 3, "kind": "parameter", "displayName": "Embedding Model", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.model.embedding.EmbeddingModel", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The EmbeddingModel to embed seg [...]
-    "embeddingStore": { "index": 4, "kind": "parameter", "displayName": "Embedding Store", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.store.embedding.EmbeddingStore<dev.langchain4j.data.segment.TextSegment>", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "desc [...]
-    "maxOverlapSize": { "index": 5, "kind": "parameter", "displayName": "Max Overlap Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 50, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How much of the previous segment each segment repeats, in characters. Over [...]
-    "maxSegmentSize": { "index": 6, "kind": "parameter", "displayName": "Max Segment Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 500, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one segment, in characters." },
-    "lazyStartProducer": { "index": 7, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...]
-    "documentSplitter": { "index": 8, "kind": "parameter", "displayName": "Document Splitter", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "dev.langchain4j.data.document.DocumentSplitter", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The DocumentSplitter deciding how a docum [...]
-    "idempotentRepository": { "index": 9, "kind": "parameter", "displayName": "Idempotent Repository", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.IdempotentRepository", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The IdempotentRepository remembering a [...]
-    "maxDocumentSize": { "index": 10, "kind": "parameter", "displayName": "Max Document Size", "group": "advanced", "label": "advanced", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one document in characters, applied to the text  [...]
-    "documentFilter": { "index": 11, "kind": "parameter", "displayName": "Document Filter", "group": "filter", "label": "filter", "required": false, "type": "object", "javaType": "org.apache.camel.Predicate", "deprecated": false, "autowired": false, "secret": false, "supportSimpleExpression": true, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "A Predicate deciding whether a deli [...]
-    "excludeId": { "index": 12, "kind": "parameter", "displayName": "Exclude Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns for document ids to skip. Exclusion wins over [...]
-    "includeId": { "index": 13, "kind": "parameter", "displayName": "Include Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns the document id must match to be ingested. A  [...]
-    "minDocumentSize": { "index": 14, "kind": "parameter", "displayName": "Min Document Size", "group": "filter", "label": "filter", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Minimum size of one document in characters; 0, the default, means no [...]
+    "contentType": { "index": 1, "kind": "parameter", "displayName": "Content Type", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "MIME type of a media body, such as audio\/wav or image\/png, handed to the embedding mode [...]
+    "documentIdHeader": { "index": 2, "kind": "parameter", "displayName": "Document Id Header", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "CamelLangChain4jIngestDocumentId", "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Name of the header carryin [...]
+    "embeddingBatchSize": { "index": 3, "kind": "parameter", "displayName": "Embedding Batch Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 32, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How many segments are embedded per request to the embedding model. [...]
+    "embeddingModel": { "index": 4, "kind": "parameter", "displayName": "Embedding Model", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.model.embedding.EmbeddingModel", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The EmbeddingModel to embed seg [...]
+    "embeddingStore": { "index": 5, "kind": "parameter", "displayName": "Embedding Store", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "dev.langchain4j.store.embedding.EmbeddingStore<dev.langchain4j.data.segment.TextSegment>", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "desc [...]
+    "maxOverlapSize": { "index": 6, "kind": "parameter", "displayName": "Max Overlap Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 50, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "How much of the previous segment each segment repeats, in characters. Over [...]
+    "maxSegmentSize": { "index": 7, "kind": "parameter", "displayName": "Max Segment Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 500, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one segment, in characters." },
+    "modality": { "index": 8, "kind": "parameter", "displayName": "Modality", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.langchain4j.ingest.IngestModality", "enum": [ "TEXT", "MEDIA" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "TEXT", "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "W [...]
+    "lazyStartProducer": { "index": 9, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...]
+    "documentSplitter": { "index": 10, "kind": "parameter", "displayName": "Document Splitter", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "dev.langchain4j.data.document.DocumentSplitter", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The DocumentSplitter deciding how a docu [...]
+    "idempotentRepository": { "index": 11, "kind": "parameter", "displayName": "Idempotent Repository", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.IdempotentRepository", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "The IdempotentRepository remembering  [...]
+    "maxDocumentSize": { "index": 12, "kind": "parameter", "displayName": "Max Document Size", "group": "advanced", "label": "advanced", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Maximum size of one document: characters of the text about to be [...]
+    "documentFilter": { "index": 13, "kind": "parameter", "displayName": "Document Filter", "group": "filter", "label": "filter", "required": false, "type": "object", "javaType": "org.apache.camel.Predicate", "deprecated": false, "autowired": false, "secret": false, "supportSimpleExpression": true, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "A Predicate deciding whether a deli [...]
+    "excludeId": { "index": 14, "kind": "parameter", "displayName": "Exclude Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns for document ids to skip. Exclusion wins over [...]
+    "includeId": { "index": 15, "kind": "parameter", "displayName": "Include Id", "group": "filter", "label": "filter", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Comma-separated list of Ant-style patterns the document id must match to be ingested. A  [...]
+    "minDocumentSize": { "index": 16, "kind": "parameter", "displayName": "Min Document Size", "group": "filter", "label": "filter", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration", "configurationField": "configuration", "description": "Minimum size of one document in characters (bytes with modality=medi [...]
   }
 }
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/main/docs/langchain4j-ingest-component.adoc b/components/camel-ai/camel-langchain4j-ingest/src/main/docs/langchain4j-ingest-component.adoc
index df4d759f9ecb..114985860897 100644
--- a/components/camel-ai/camel-langchain4j-ingest/src/main/docs/langchain4j-ingest-component.adoc
+++ b/components/camel-ai/camel-langchain4j-ingest/src/main/docs/langchain4j-ingest-component.adoc
@@ -201,6 +201,56 @@ memory- and CPU-expensive and a small file can still parse into a lot of text. A
 document fails the exchange like any other error (see below) and releases its dedup claim, so
 a trimmed re-delivery under the same id still ingests.
 
+=== Ingesting media: images, audio, video and PDF
+
+With `modality=media` the body is read as bytes and embedded whole, as one vector, by a
+multimodal embedding model. The model must list the medium (`IMAGE`, `AUDIO`, `VIDEO` or
+`PDF`) in its `supportedContentTypes()`; a text-only model fails the start. The vector is
+stored under a placeholder segment whose text is the document id, with the same identity
+metadata as text segments plus the MIME type under `camel_ingest_content_type`, so
+retrieval and citation work unchanged and a retriever on a shared store can tell a media
+hit from text.
+
+[source,java]
+----
+from("file:/var/data/photos?noop=true&recursive=true&readLock=changed&antInclude=**/*.jpg,**/*.png")
+    .to("langchain4j-ingest:photos?modality=media&documentIdHeader=CamelFileName");
+----
+
+Leave `charset` unset on the consumer: a file consumer with one converts the file through
+a Reader, which corrupts media. Keep other files out of the folder with the consumer's
+`antInclude`, as above, or the endpoint's `includeId` and `excludeId`: an unsupported file
+fails the exchange and is retried on every poll.
+
+The medium is taken from the MIME type: the `contentType` option when set, otherwise the
+document id's file extension looked up in Camel's MIME table (`MimeTypeHelper` in
+camel-util: wav, mp3, png, mp4, pdf and the like). Message headers are never consulted. An
+id ending in a query string, a URL say, has no usable extension and needs `contentType`. A
+document whose type is unknown, is not a medium or is one the model does not support fails
+the exchange before its dedup claim and before its body is read, so a mixed folder fails
+per file. A `.webm` file counts as video; a browser audio recording needs
+`contentType=audio/webm`. The family decides the medium and the provider decides the
+formats within it, so an `image/svg+xml` file passes as an image and fails at the
+provider. A `contentType` the model does not support, or one set with `modality=text`,
+fails the start.
+
+Nothing is split: `maxSegmentSize`, `maxOverlapSize` and `embeddingBatchSize` do not
+apply, a configured `documentSplitter` fails the start, and `maxDocumentSize` and
+`minDocumentSize` count bytes. Base64 encoding adds a third, so set `maxDocumentSize`
+against the model's request limit. A file consumer announces the length in
+`CamelFileLength`, which is checked against the cap before the file is read; any other
+body is checked once it is in memory. A `documentFilter` runs before the body is read and
+sees it as delivered, a file or stream rather than text, so filter on headers or the id.
+
+Several LangChain4j providers embed images (Jina, Voyage AI, Cohere, Google Gemini and
+Bedrock Titan among them); none embeds audio yet, so an audio model is typically a custom
+`EmbeddingModel` calling a service such as Wav2Vec2 or CLAP. The same bean embeds the
+query at retrieval time. A model with an `EmbeddingModelListener` attached is refused at
+start: LangChain4j 1.20 builds the listener context from the input text, which media lacks.
+Only a listener added by hand is affected; the Quarkus and Spring Boot LangChain4j
+integrations attach none to embedding models today. Media embedding goes through
+LangChain4j's `EmbeddingRequest` API, still experimental in 1.20, so it may change.
+
 === When ingestion fails
 
 A failure while splitting, embedding or storing — a rate-limited model, an unreachable store —
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/IngestModality.java b/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/IngestModality.java
new file mode 100644
index 000000000000..21d6e1725c5b
--- /dev/null
+++ b/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/IngestModality.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.langchain4j.ingest;
+
+/**
+ * What the message body is, and therefore how it is embedded.
+ */
+public enum IngestModality {
+
+    /** Text: read as a String, split into segments and embedded segment by segment. */
+    TEXT,
+
+    /**
+     * Media: audio, an image, video or a PDF, read as bytes and embedded whole, as one vector, by an embedding model
+     * that declares the matching {@code ContentType} among its supported content types. The medium is told by the MIME
+     * type, from the {@code contentType} option or the document id's file extension.
+     */
+    MEDIA
+}
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/IngestService.java b/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/IngestService.java
index a5322d1726f9..4c629fbc7cf1 100644
--- a/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/IngestService.java
+++ b/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/IngestService.java
@@ -131,10 +131,6 @@ class IngestService {
         return new IngestResult(pipeline, documentId, batching.segmentsEmbedded(), IngestResult.Outcome.INGESTED);
     }
 
-    public String pipeline() {
-        return pipeline;
-    }
-
     /**
      * Every LangChain4j-provided splitter propagates the document metadata onto its segments, but a custom
      * {@code DocumentSplitter} might not — and a segment without the identity stamps would break citation and the
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngest.java b/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngest.java
index dc6211263acc..93f184d65142 100644
--- a/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngest.java
+++ b/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngest.java
@@ -44,6 +44,12 @@ public final class LangChain4jIngest {
      */
     public static final String METADATA_DOCUMENT_ID = "camel_ingest_document_id";
 
+    /**
+     * Metadata key carrying the MIME type of a media document on its placeholder segment, absent on text segments, so a
+     * retriever on a shared store can tell a media hit from text.
+     */
+    public static final String METADATA_CONTENT_TYPE = "camel_ingest_content_type";
+
     private LangChain4jIngest() {
     }
 }
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestConfiguration.java b/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestConfiguration.java
index 600fb9263529..b365382cf707 100644
--- a/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestConfiguration.java
+++ b/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestConfiguration.java
@@ -62,11 +62,12 @@ public class LangChain4jIngestConfiguration implements Cloneable {
               defaultValue = "32")
     private int embeddingBatchSize = 32;
 
-    @UriParam(description = "Maximum size of one document in characters, applied to the text about to be split;"
-                            + " 0, the default, means no limit. The pipeline holds a document in memory whole, so"
+    @UriParam(description = "Maximum size of one document: characters of the text about to be split, or bytes of"
+                            + " a media body with modality=media; 0, the default, means no limit. The pipeline holds a document in memory whole, so"
                             + " the cap is the protection against oversized - on a consumer-fed pipeline,"
                             + " attacker-sized - payloads. An oversized document fails the exchange cleanly and,"
-                            + " with a repository configured, releases its dedup claim.",
+                            + " with a repository configured, releases its dedup claim. With modality=media the size a file"
+                            + " consumer announces in CamelFileLength is checked before the body is read.",
               defaultValue = "0", label = "advanced")
     private int maxDocumentSize;
 
@@ -104,7 +105,8 @@ public class LangChain4jIngestConfiguration implements Cloneable {
               label = "filter")
     private String excludeId;
 
-    @UriParam(description = "Minimum size of one document in characters; 0, the default, means no minimum. A"
+    @UriParam(description = "Minimum size of one document in characters (bytes with modality=media); 0, the"
+                            + " default, means no minimum. A"
                             + " shorter document - boilerplate too small to carry retrievable content - is answered"
                             + " with a filtered result instead of being written, and releases its dedup claim like"
                             + " a blank one.",
@@ -115,8 +117,9 @@ public class LangChain4jIngestConfiguration implements Cloneable {
                             + " evaluated with the message body available. A rejected delivery is answered with a"
                             + " filtered result and releases its dedup claim. Runs after the id patterns and after"
                             + " the dedup claim, so a duplicate is answered skipped without the filter being"
-                            + " evaluated. Not looked up by type on purpose - an application may hold unrelated"
-                            + " predicates.",
+                            + " evaluated. The body is still as the consumer delivered it, a file or stream say, not"
+                            + " yet read as text or bytes. Not looked up by type on purpose -"
+                            + " an application may hold unrelated predicates.",
               label = "filter")
     private Predicate documentFilter;
 
@@ -134,6 +137,29 @@ public class LangChain4jIngestConfiguration implements Cloneable {
               label = "advanced")
     private IdempotentRepository idempotentRepository;
 
+    @UriParam(description = "What the message body is. text, the default, is read as a String, split into segments"
+                            + " and embedded segment by segment. media is read as bytes and embedded whole, as one"
+                            + " vector: audio, an image, video or a PDF, told apart by the MIME type, each needing"
+                            + " an embedding model whose supportedContentTypes() include the matching type - the"
+                            + " endpoint refuses to start with a text-only model. In media mode the splitter options"
+                            + " and embeddingBatchSize do not apply, documentSplitter must not be set, and"
+                            + " maxDocumentSize and minDocumentSize count bytes.",
+              defaultValue = "TEXT")
+    private IngestModality modality = IngestModality.TEXT;
+
+    @UriParam(description = "MIME type of a media body, such as audio/wav or image/png, handed to the embedding"
+                            + " model; matched case-insensitively, parameters after a semicolon are dropped. When"
+                            + " not set, it is derived from the document id's file extension through Camel's own"
+                            + " MIME table (the x- audio variants rewritten to their registered form): wav, mp3,"
+                            + " flac, ogg, opus, m4a, aac, aiff, png, jpg, gif, webp, mp4, mov, webm (video), pdf"
+                            + " and every other extension the table knows. A document whose type cannot be"
+                            + " determined, whose type is no medium (text/plain, say), or whose medium the model"
+                            + " does not declare, fails the exchange before its dedup claim and before its body is"
+                            + " read. Only valid with modality=media - with"
+                            + " modality=text the endpoint refuses to start, the option being a sign that"
+                            + " modality=media was forgotten.")
+    private String contentType;
+
     public EmbeddingStore<TextSegment> getEmbeddingStore() {
         return embeddingStore;
     }
@@ -194,7 +220,7 @@ public class LangChain4jIngestConfiguration implements Cloneable {
     }
 
     /**
-     * Sets the maximum size of one document in characters; 0 means no limit.
+     * Sets the maximum size of one document in characters, or bytes with modality=media; 0 means no limit.
      */
     public void setMaxDocumentSize(int maxDocumentSize) {
         this.maxDocumentSize = maxDocumentSize;
@@ -249,7 +275,7 @@ public class LangChain4jIngestConfiguration implements Cloneable {
     }
 
     /**
-     * Sets the minimum size of one document in characters; 0 means no minimum.
+     * Sets the minimum size of one document in characters, or bytes with modality=media; 0 means no minimum.
      */
     public void setMinDocumentSize(int minDocumentSize) {
         this.minDocumentSize = minDocumentSize;
@@ -277,6 +303,29 @@ public class LangChain4jIngestConfiguration implements Cloneable {
         this.idempotentRepository = idempotentRepository;
     }
 
+    public IngestModality getModality() {
+        return modality;
+    }
+
+    /**
+     * Sets what the message body is: text, split and embedded segment by segment, or media (audio, an image, video or a
+     * PDF, told apart by the MIME type), embedded whole.
+     */
+    public void setModality(IngestModality modality) {
+        this.modality = modality;
+    }
+
+    public String getContentType() {
+        return contentType;
+    }
+
+    /**
+     * Sets the MIME type of a media body; when not set, it is derived from the document id's file extension.
+     */
+    public void setContentType(String contentType) {
+        this.contentType = contentType;
+    }
+
     // ************************
     //
     // Clone
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestProducer.java b/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestProducer.java
index dc68870ede1c..efc3cb0566a0 100644
--- a/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestProducer.java
+++ b/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestProducer.java
@@ -17,22 +17,27 @@
 package org.apache.camel.component.langchain4j.ingest;
 
 import java.util.Arrays;
+import java.util.Locale;
 import java.util.Set;
 
+import dev.langchain4j.data.message.ContentType;
 import dev.langchain4j.data.segment.TextSegment;
 import dev.langchain4j.model.embedding.EmbeddingModel;
 import dev.langchain4j.store.embedding.EmbeddingStore;
 import org.apache.camel.CamelContextAware;
 import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
 import org.apache.camel.Predicate;
 import org.apache.camel.spi.IdempotentRepository;
 import org.apache.camel.support.DefaultProducer;
 import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.util.AntPathMatcher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Splits the message body into segments, embeds them in batches and writes them to the embedding store; the message
- * body is replaced with the {@link IngestResult}.
+ * body is replaced with the {@link IngestResult}. With {@link IngestModality#MEDIA} the body is embedded whole instead.
  *
  * <p>
  * With an {@code idempotentRepository} configured, the producer claims the document id before writing: a duplicate
@@ -45,11 +50,20 @@ import org.apache.camel.util.AntPathMatcher;
  */
 public class LangChain4jIngestProducer extends DefaultProducer {
 
+    private static final Logger LOG = LoggerFactory.getLogger(LangChain4jIngestProducer.class);
+
     private final LangChain4jIngestEndpoint endpoint;
     private final LangChain4jIngestConfiguration configuration;
-    private IngestService service;
+    /** One of the two is set at start, by modality. */
+    private IngestService textService;
+    private MediaIngestService mediaService;
     private String[] includeIds;
     private String[] excludeIds;
+    private boolean media;
+    /** The configured contentType, normalised; null when unset. Content-type headers are ignored on purpose. */
+    private String contentType;
+    /** What the model declares it can embed; media mode only. */
+    private Set<ContentType> supportedTypes;
 
     public LangChain4jIngestProducer(LangChain4jIngestEndpoint endpoint) {
         super(endpoint);
@@ -71,9 +85,24 @@ public class LangChain4jIngestProducer extends DefaultProducer {
                     "Ingestion pipeline '" + pipeline + "': documentIdHeader must not be blank. Omit the option for"
                                                + " the default, or name the header carrying the document id.");
         }
+        media = configuration.getModality() == IngestModality.MEDIA;
+        contentType = normalizeContentType(configuration.getContentType());
+        if (!media && contentType != null) {
+            // silently ignoring it would embed media bytes as text on a forgotten modality=media
+            throw new IllegalArgumentException(
+                    "Ingestion pipeline '" + pipeline + "': contentType only applies to modality=media (got '"
+                                               + configuration.getContentType() + "'). Set modality=media, or"
+                                               + " remove the option.");
+        }
+        if (media && configuration.getDocumentSplitter() != null) {
+            throw new IllegalArgumentException(
+                    "Ingestion pipeline '" + pipeline + "': documentSplitter does not apply to modality=media, where"
+                                               + " a document is embedded whole. Remove the option.");
+        }
         // the splitter bounds parameterize the default recursive splitter only: with a custom
-        // documentSplitter they are documented as ignored, so they are not validated either
-        if (configuration.getDocumentSplitter() == null
+        // documentSplitter, or in media mode, they are documented as ignored, so they are not
+        // validated either
+        if (!media && configuration.getDocumentSplitter() == null
                 && (configuration.getMaxSegmentSize() <= 0 || configuration.getMaxOverlapSize() < 0
                         || configuration.getMaxOverlapSize() >= configuration.getMaxSegmentSize())) {
             throw new IllegalArgumentException(
@@ -83,7 +112,9 @@ public class LangChain4jIngestProducer extends DefaultProducer {
                                                + " / " + configuration.getMaxOverlapSize() + ")");
         }
 
-        if (configuration.getEmbeddingBatchSize() < 1) {
+        // like the splitter bounds, the batch size does not apply in media mode, so it is not
+        // validated there either
+        if (!media && configuration.getEmbeddingBatchSize() < 1) {
             throw new IllegalArgumentException(
                     "Ingestion pipeline '" + pipeline + "': embeddingBatchSize must be positive (got "
                                                + configuration.getEmbeddingBatchSize() + ")");
@@ -121,15 +152,53 @@ public class LangChain4jIngestProducer extends DefaultProducer {
                 = resolve(EmbeddingStore.class, configuration.getEmbeddingStore(), "embedding store", "embeddingStore");
         EmbeddingModel model
                 = resolve(EmbeddingModel.class, configuration.getEmbeddingModel(), "embedding model", "embeddingModel");
-        service = configuration.getDocumentSplitter() != null
-                ? new IngestService(
-                        pipeline, store, model, configuration.getDocumentSplitter(),
-                        configuration.getEmbeddingBatchSize(), configuration.getMaxDocumentSize(),
-                        configuration.getMinDocumentSize())
-                : new IngestService(
-                        pipeline, store, model, configuration.getMaxSegmentSize(),
-                        configuration.getMaxOverlapSize(), configuration.getEmbeddingBatchSize(),
-                        configuration.getMaxDocumentSize(), configuration.getMinDocumentSize());
+        if (media) {
+            // LangChain4j would reject the first request anyway, but only once a document has
+            // been read; the misconfiguration is better reported before any is. allMatch is
+            // vacuously true for an empty set, which declares no medium either
+            supportedTypes = model.supportedContentTypes();
+            if (supportedTypes == null || supportedTypes.stream().allMatch(type -> type == ContentType.TEXT)) {
+                throw new IllegalArgumentException(
+                        "Ingestion pipeline '" + pipeline + "': modality=media, but the embedding model "
+                                                   + model.getClass().getName() + " does not support any media input"
+                                                   + " (its supportedContentTypes() are " + supportedTypes
+                                                   + "). Configure an EmbeddingModel that declares AUDIO, IMAGE,"
+                                                   + " VIDEO or PDF.");
+            }
+            // with listeners attached, LangChain4j 1.20 builds the listener context from the
+            // input text, which media lacks, so every media request would fail inside the
+            // library with an opaque "text cannot be null or blank". addListener wraps the model
+            // in ListeningEmbeddingModel, a package-private class that does not expose its
+            // listeners, hence the check by name beside the listeners() one
+            // TODO CAMEL-25332: drop the class-name check once a LangChain4j release after 1.20 returns the
+            // wrapped listeners from listeners() or builds the context without text
+            if ("dev.langchain4j.model.embedding.ListeningEmbeddingModel".equals(model.getClass().getName())
+                    || (model.listeners() != null && !model.listeners().isEmpty())) {
+                throw new IllegalArgumentException(
+                        "Ingestion pipeline '" + pipeline + "': modality=media, but the embedding model has"
+                                                   + " EmbeddingModelListeners attached. LangChain4j builds the"
+                                                   + " listener context from the input text, which media lacks, so"
+                                                   + " every request would fail; use a model without listeners.");
+            }
+            if (contentType != null) {
+                // one type for every document: a medium the model lacks fails here, not per document
+                requireSupported(contentType, "the contentType option");
+            }
+            mediaService = new MediaIngestService(
+                    pipeline, store, model, configuration.getMaxDocumentSize(), configuration.getMinDocumentSize());
+            LOG.debug("Ingestion pipeline '{}': modality=media, maxDocumentSize={} and minDocumentSize={} count bytes",
+                    pipeline, configuration.getMaxDocumentSize(), configuration.getMinDocumentSize());
+        } else if (configuration.getDocumentSplitter() != null) {
+            textService = new IngestService(
+                    pipeline, store, model, configuration.getDocumentSplitter(),
+                    configuration.getEmbeddingBatchSize(), configuration.getMaxDocumentSize(),
+                    configuration.getMinDocumentSize());
+        } else {
+            textService = new IngestService(
+                    pipeline, store, model, configuration.getMaxSegmentSize(),
+                    configuration.getMaxOverlapSize(), configuration.getEmbeddingBatchSize(),
+                    configuration.getMaxDocumentSize(), configuration.getMinDocumentSize());
+        }
 
         IdempotentRepository repository = configuration.getIdempotentRepository();
         if (repository != null) {
@@ -152,10 +221,19 @@ public class LangChain4jIngestProducer extends DefaultProducer {
             exchange.getMessage().setBody(filtered(documentId));
             return;
         }
+        // the media type depends on the id alone, so it is resolved here as well: a document
+        // whose type cannot be told, or whose medium the model lacks, never occupies its id and
+        // never pays for the payload
+        String mimeType = null;
+        ContentType medium = null;
+        if (media) {
+            mimeType = mediaTypeOf(documentId);
+            medium = requireSupported(mimeType, "document '" + documentId + "'");
+        }
 
         IdempotentRepository repository = configuration.getIdempotentRepository();
         if (repository == null) {
-            exchange.getMessage().setBody(ingestUnlessFiltered(exchange, documentId));
+            exchange.getMessage().setBody(ingestUnlessFiltered(exchange, documentId, mimeType, medium));
             return;
         }
 
@@ -166,12 +244,12 @@ public class LangChain4jIngestProducer extends DefaultProducer {
         // consumer EIP - a repository overriding only those variants is not bypassed
         if (!repository.add(exchange, documentId)) {
             exchange.getMessage().setBody(
-                    new IngestResult(service.pipeline(), documentId, 0, IngestResult.Outcome.SKIPPED));
+                    new IngestResult(endpoint.getPipelineName(), documentId, 0, IngestResult.Outcome.SKIPPED));
             return;
         }
         IngestResult result;
         try {
-            result = ingestUnlessFiltered(exchange, documentId);
+            result = ingestUnlessFiltered(exchange, documentId, mimeType, medium);
         } catch (Exception e) {
             // a failed write must not keep the claim, or the delivery could never be retried.
             // An Error (an OutOfMemoryError, say) is deliberately not caught: under a VM-level
@@ -200,12 +278,27 @@ public class LangChain4jIngestProducer extends DefaultProducer {
      * The content filters, in claim scope: the documentFilter predicate sees the body, and minDocumentSize (inside
      * {@link IngestService}) needs the text - both run after the dedup claim, unlike the id patterns.
      */
-    private IngestResult ingestUnlessFiltered(Exchange exchange, String documentId) {
+    private IngestResult ingestUnlessFiltered(Exchange exchange, String documentId, String mimeType, ContentType medium)
+            throws InvalidPayloadException {
         Predicate filter = configuration.getDocumentFilter();
         if (filter != null && !filter.matches(exchange)) {
             return filtered(documentId);
         }
-        return service.ingest(documentId, exchange.getMessage().getBody(String.class));
+        if (media) {
+            // a file consumer announces the size up front: an oversized file is refused before it is read
+            Long announced = exchange.getMessage().getHeader(Exchange.FILE_LENGTH, Long.class);
+            if (announced != null) {
+                mediaService.checkSize(documentId, announced);
+            }
+            // bytes, not a String - a charset conversion would corrupt them. A null body stays
+            // EMPTY; anything else must convert, so a wrong body type (a POJO, say) fails as an
+            // InvalidPayloadException instead of being answered EMPTY by a silent null conversion
+            byte[] bytes = exchange.getMessage().getBody() == null
+                    ? null
+                    : exchange.getMessage().getMandatoryBody(byte[].class);
+            return mediaService.ingest(documentId, bytes, mimeType, medium);
+        }
+        return textService.ingest(documentId, exchange.getMessage().getBody(String.class));
     }
 
     private IngestResult filtered(String documentId) {
@@ -214,6 +307,55 @@ public class LangChain4jIngestProducer extends DefaultProducer {
         return new IngestResult(endpoint.getPipelineName(), documentId, 0, IngestResult.Outcome.FILTERED);
     }
 
+    /** The configured contentType, else the type the document id's file extension implies. */
+    private String mediaTypeOf(String documentId) {
+        if (contentType != null) {
+            return contentType;
+        }
+        String type = MediaTypes.fromDocumentId(documentId);
+        if (type == null) {
+            throw new IllegalArgumentException(
+                    "Ingestion pipeline '" + endpoint.getPipelineName() + "': cannot tell the media type of document '"
+                                               + documentId + "' from its extension. Set the contentType endpoint"
+                                               + " option.");
+        }
+        return type;
+    }
+
+    /**
+     * The LangChain4j content type a MIME type belongs to, checked against what the model declares so a medium it
+     * cannot embed is refused before the body is read; fails naming what needed the type.
+     */
+    private ContentType requireSupported(String mimeType, String what) {
+        ContentType type = MediaTypes.contentTypeOf(mimeType);
+        if (type == null) {
+            throw new IllegalArgumentException(
+                    "Ingestion pipeline '" + endpoint.getPipelineName() + "': '" + mimeType + "' (" + what
+                                               + ") is not a media type this pipeline can embed: audio/*, image/*,"
+                                               + " video/* or application/pdf.");
+        }
+        if (!supportedTypes.contains(type)) {
+            throw new IllegalArgumentException(
+                    "Ingestion pipeline '" + endpoint.getPipelineName() + "': the embedding model does not support "
+                                               + type + " input, needed for " + what + " (its supportedContentTypes()"
+                                               + " are " + supportedTypes + ").");
+        }
+        return type;
+    }
+
+    /**
+     * Trimmed, lower-cased (RFC 2045 types are case-insensitive) and without parameters, the form an embedding provider
+     * expects: {@code Audio/WAV; rate=16000} is handed to the model as {@code audio/wav}. Blank means unset.
+     */
+    private static String normalizeContentType(String value) {
+        if (value == null) {
+            return null;
+        }
+        int semicolon = value.indexOf(';');
+        String type = (semicolon < 0 ? value : value.substring(0, semicolon)).trim().toLowerCase(Locale.ROOT);
+        return type.isEmpty() ? null : type;
+    }
+
     /** Exclusion wins over inclusion; with includeId set, only matching ids pass. */
     private boolean idAccepted(String documentId) {
         if (AntPathMatcher.INSTANCE.anyMatch(excludeIds, documentId)) {
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/MediaIngestService.java b/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/MediaIngestService.java
new file mode 100644
index 000000000000..37efc2326406
--- /dev/null
+++ b/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/MediaIngestService.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.langchain4j.ingest;
+
+import java.util.Base64;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import dev.langchain4j.data.document.Metadata;
+import dev.langchain4j.data.embedding.Embedding;
+import dev.langchain4j.data.message.Content;
+import dev.langchain4j.data.message.ContentType;
+import dev.langchain4j.data.segment.TextSegment;
+import dev.langchain4j.model.embedding.EmbeddingModel;
+import dev.langchain4j.model.embedding.request.EmbeddingRequest;
+import dev.langchain4j.store.embedding.EmbeddingStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Embeds a media document — audio, an image, video or a PDF, told by the MIME type — whole, as one vector, and stores
+ * it with a placeholder segment: its text is the document id and it carries the same identity metadata as text segments
+ * plus the MIME type, so retrieval cites it the same way and can tell it from text. Nothing is split or batched, which
+ * is why this is a class of its own beside {@link IngestService} rather than a mode of it; the size checks count bytes.
+ * Embeds through LangChain4j's {@code EmbeddingRequest} API, still experimental in 1.20.
+ */
+final class MediaIngestService {
+
+    private static final Logger LOG = LoggerFactory.getLogger(MediaIngestService.class);
+
+    private final String pipeline;
+    private final EmbeddingStore<TextSegment> store;
+    private final EmbeddingModel model;
+    private final int maxDocumentSize;
+    private final int minDocumentSize;
+
+    MediaIngestService(String pipeline, EmbeddingStore<TextSegment> store, EmbeddingModel model, int maxDocumentSize,
+                       int minDocumentSize) {
+        this.pipeline = pipeline;
+        this.store = store;
+        this.model = model;
+        this.maxDocumentSize = maxDocumentSize;
+        this.minDocumentSize = minDocumentSize;
+    }
+
+    /**
+     * @param documentId never null or blank: the producer resolves and checks it before calling
+     * @param medium     the LangChain4j content type the MIME type belongs to, resolved and checked against the model
+     *                   by the producer before the bytes were read
+     */
+    IngestResult ingest(String documentId, byte[] bytes, String mimeType, ContentType medium) {
+        if (bytes == null || bytes.length == 0) {
+            return new IngestResult(pipeline, documentId, 0, IngestResult.Outcome.EMPTY);
+        }
+        if (minDocumentSize > 0 && bytes.length < minDocumentSize) {
+            return new IngestResult(pipeline, documentId, 0, IngestResult.Outcome.FILTERED);
+        }
+        checkSize(documentId, bytes.length);
+
+        Map<String, Object> identity = new LinkedHashMap<>();
+        identity.put(LangChain4jIngest.METADATA_PIPELINE, pipeline);
+        identity.put(LangChain4jIngest.METADATA_DOCUMENT_ID, documentId);
+        identity.put(LangChain4jIngest.METADATA_CONTENT_TYPE, mimeType);
+
+        Content content = MediaTypes.contentOf(medium, Base64.getEncoder().encodeToString(bytes), mimeType);
+        // one input, so one embedding is expected back. EmbeddingRequest and EmbeddingResponse are
+        // experimental API in LangChain4j 1.20, hence the shape is asserted rather than assumed
+        List<Embedding> embeddings = model.embed(EmbeddingRequest.builder().input(content).build()).embeddings();
+        if (embeddings == null || embeddings.size() != 1) {
+            throw new IllegalStateException(
+                    "Ingestion pipeline '" + pipeline + "': the embedding model returned "
+                                            + (embeddings == null ? 0 : embeddings.size())
+                                            + " embeddings for the single media document '" + documentId + "'");
+        }
+        store.add(embeddings.get(0), TextSegment.from(documentId, Metadata.from(identity)));
+
+        LOG.debug("Ingestion pipeline '{}': wrote 1 vector of {} document '{}'", pipeline, mimeType, documentId);
+        return new IngestResult(pipeline, documentId, 1, IngestResult.Outcome.INGESTED);
+    }
+
+    /**
+     * Fails an oversized document; the producer also applies it to the size a file consumer announces before the read.
+     */
+    void checkSize(String documentId, long size) {
+        if (maxDocumentSize > 0 && size > maxDocumentSize) {
+            throw new IllegalArgumentException(
+                    "Ingestion pipeline '" + pipeline + "': document '" + documentId + "' exceeds maxDocumentSize ("
+                                               + size + " > " + maxDocumentSize + " bytes)");
+        }
+    }
+}
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/MediaTypes.java b/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/MediaTypes.java
new file mode 100644
index 000000000000..7da9ad1bde21
--- /dev/null
+++ b/components/camel-ai/camel-langchain4j-ingest/src/main/java/org/apache/camel/component/langchain4j/ingest/MediaTypes.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.langchain4j.ingest;
+
+import java.util.Map;
+
+import dev.langchain4j.data.message.AudioContent;
+import dev.langchain4j.data.message.Content;
+import dev.langchain4j.data.message.ContentType;
+import dev.langchain4j.data.message.ImageContent;
+import dev.langchain4j.data.message.PdfFileContent;
+import dev.langchain4j.data.message.VideoContent;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.MimeTypeHelper;
+
+/**
+ * The media a {@code modality=media} pipeline can embed: which MIME type a file extension implies, which LangChain4j
+ * content type a MIME type belongs to, and the content object handed to the model.
+ */
+final class MediaTypes {
+
+    /**
+     * Camel's MIME table returns the {@code x-} variants for a few audio extensions, which embedding providers do not
+     * list; those are rewritten to their registered form.
+     */
+    private static final Map<String, String> AUDIO_FIXUPS = Map.of(
+            "audio/x-wav", "audio/wav",
+            "audio/x-flac", "audio/flac",
+            "audio/x-aac", "audio/aac",
+            "audio/x-aiff", "audio/aiff");
+
+    private MediaTypes() {
+    }
+
+    /** The MIME type the document id's file extension implies, per Camel's own MIME table, or null without one. */
+    static String fromDocumentId(String documentId) {
+        if (FileUtil.onlyExt(documentId, true) == null) {
+            // the probe falls back to the whole name, which would type a bare "mp3" id as audio
+            return null;
+        }
+        String type = MimeTypeHelper.probeMimeType(documentId);
+        return type == null ? null : AUDIO_FIXUPS.getOrDefault(type, type);
+    }
+
+    /** The LangChain4j content type a MIME type belongs to, or null for a type no media pipeline can embed. */
+    static ContentType contentTypeOf(String mimeType) {
+        if (mimeType.startsWith("audio/")) {
+            return ContentType.AUDIO;
+        }
+        if (mimeType.startsWith("image/")) {
+            return ContentType.IMAGE;
+        }
+        if (mimeType.startsWith("video/")) {
+            return ContentType.VIDEO;
+        }
+        if ("application/pdf".equals(mimeType)) {
+            return ContentType.PDF;
+        }
+        return null;
+    }
+
+    /** The content handed to the model: the bytes as base64, typed by the MIME type and its resolved content type. */
+    static Content contentOf(ContentType type, String base64, String mimeType) {
+        return switch (type) {
+            case AUDIO -> AudioContent.from(base64, mimeType);
+            case IMAGE -> ImageContent.from(base64, mimeType);
+            case VIDEO -> VideoContent.from(base64, mimeType);
+            case PDF -> PdfFileContent.from(base64, mimeType);
+            case TEXT -> throw new IllegalArgumentException("Not a media type: " + mimeType);
+        };
+    }
+}
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/DeterministicEmbeddingModel.java b/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/DeterministicEmbeddingModel.java
index aa1c4e6f37e2..2544c1847375 100644
--- a/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/DeterministicEmbeddingModel.java
+++ b/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/DeterministicEmbeddingModel.java
@@ -49,12 +49,7 @@ class DeterministicEmbeddingModel implements EmbeddingModel {
             if (segment.text().contains(POISON)) {
                 throw new IllegalStateException("test-induced embedding failure: " + POISON);
             }
-            Random random = new Random(segment.text().hashCode());
-            float[] vector = new float[dimension];
-            for (int i = 0; i < dimension; i++) {
-                vector[i] = random.nextFloat();
-            }
-            embeddings.add(Embedding.from(vector));
+            embeddings.add(embedding(segment.text()));
         }
         return Response.from(embeddings);
     }
@@ -68,4 +63,14 @@ class DeterministicEmbeddingModel implements EmbeddingModel {
     public int embedAllCalls() {
         return embedAllCalls.get();
     }
+
+    /** The vector for a seed: same seed, same vector. */
+    protected Embedding embedding(String seed) {
+        Random random = new Random(seed.hashCode());
+        float[] vector = new float[dimension];
+        for (int i = 0; i < dimension; i++) {
+            vector[i] = random.nextFloat();
+        }
+        return Embedding.from(vector);
+    }
 }
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/DeterministicMediaEmbeddingModel.java b/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/DeterministicMediaEmbeddingModel.java
new file mode 100644
index 000000000000..165cac9bc8e2
--- /dev/null
+++ b/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/DeterministicMediaEmbeddingModel.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.langchain4j.ingest;
+
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import dev.langchain4j.data.embedding.Embedding;
+import dev.langchain4j.data.message.AudioContent;
+import dev.langchain4j.data.message.Content;
+import dev.langchain4j.data.message.ContentType;
+import dev.langchain4j.data.message.ImageContent;
+import dev.langchain4j.data.message.PdfFileContent;
+import dev.langchain4j.data.message.VideoContent;
+import dev.langchain4j.model.embedding.request.EmbeddingInput;
+import dev.langchain4j.model.embedding.request.EmbeddingRequest;
+import dev.langchain4j.model.embedding.response.EmbeddingResponse;
+
+/**
+ * Deterministic fake that also accepts media: same bytes, same vector. Records the content and MIME types it was
+ * handed.
+ */
+class DeterministicMediaEmbeddingModel extends DeterministicEmbeddingModel {
+
+    private final Set<ContentType> supported;
+    private final List<String> mimeTypes = new CopyOnWriteArrayList<>();
+    private final List<ContentType> contentTypes = new CopyOnWriteArrayList<>();
+
+    /** Supports every medium LangChain4j knows. */
+    DeterministicMediaEmbeddingModel(int dimension) {
+        this(dimension, Set.of(ContentType.TEXT, ContentType.IMAGE, ContentType.AUDIO, ContentType.VIDEO,
+                ContentType.PDF));
+    }
+
+    /** Supports the given content types only, for the tests of what a narrower model is refused. */
+    DeterministicMediaEmbeddingModel(int dimension, Set<ContentType> supported) {
+        super(dimension);
+        this.supported = supported;
+    }
+
+    @Override
+    public Set<ContentType> supportedContentTypes() {
+        return supported;
+    }
+
+    @Override
+    public EmbeddingResponse doEmbed(EmbeddingRequest request) {
+        List<Embedding> embeddings = new ArrayList<>(request.inputs().size());
+        for (EmbeddingInput input : request.inputs()) {
+            Content media = mediaOf(input);
+            if (media == null) {
+                embeddings.add(embedding(input.text()));
+            } else {
+                contentTypes.add(media.type());
+                mimeTypes.add(mimeTypeOf(media));
+                embeddings.add(embedding(base64Of(media)));
+            }
+        }
+        return EmbeddingResponse.builder().embeddings(embeddings).build();
+    }
+
+    /** The vector this fake produces for the given bytes: what a query by the same file embeds to. */
+    Embedding embeddingOf(byte[] bytes) {
+        return embedding(Base64.getEncoder().encodeToString(bytes));
+    }
+
+    /** MIME types handed to the model, in call order. */
+    List<String> mimeTypes() {
+        return mimeTypes;
+    }
+
+    /** Content types handed to the model, in call order. */
+    List<ContentType> contentTypes() {
+        return contentTypes;
+    }
+
+    private static Content mediaOf(EmbeddingInput input) {
+        if (input.contents() == null) {
+            return null;
+        }
+        for (Content content : input.contents()) {
+            if (content.type() != ContentType.TEXT) {
+                return content;
+            }
+        }
+        return null;
+    }
+
+    private static String mimeTypeOf(Content content) {
+        return switch (content.type()) {
+            case AUDIO -> ((AudioContent) content).audio().mimeType();
+            case IMAGE -> ((ImageContent) content).image().mimeType();
+            case VIDEO -> ((VideoContent) content).video().mimeType();
+            case PDF -> ((PdfFileContent) content).pdfFile().mimeType();
+            case TEXT -> throw new IllegalArgumentException("not media: " + content);
+        };
+    }
+
+    private static String base64Of(Content content) {
+        return switch (content.type()) {
+            case AUDIO -> ((AudioContent) content).audio().base64Data();
+            case IMAGE -> ((ImageContent) content).image().base64Data();
+            case VIDEO -> ((VideoContent) content).video().base64Data();
+            case PDF -> ((PdfFileContent) content).pdfFile().base64Data();
+            case TEXT -> throw new IllegalArgumentException("not media: " + content);
+        };
+    }
+}
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestMediaFolderTest.java b/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestMediaFolderTest.java
new file mode 100644
index 000000000000..8be050a6b6eb
--- /dev/null
+++ b/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestMediaFolderTest.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.langchain4j.ingest;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+import dev.langchain4j.data.message.ContentType;
+import dev.langchain4j.data.segment.TextSegment;
+import dev.langchain4j.store.embedding.EmbeddingMatch;
+import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
+import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.NotifyBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The case the single {@code media} value exists for: one folder holding several media, one file consumer, one store.
+ * The bodies are fake except the WAV, see {@link LangChain4jIngestMediaTest}: the medium comes from the file name.
+ */
+class LangChain4jIngestMediaFolderTest extends CamelTestSupport {
+
+    @TempDir
+    Path folder;
+
+    @BindToRegistry("allStore")
+    private final InMemoryEmbeddingStore<TextSegment> allStore = new InMemoryEmbeddingStore<>();
+
+    @BindToRegistry("allModel")
+    private final DeterministicMediaEmbeddingModel allModel = new DeterministicMediaEmbeddingModel(16);
+
+    @BindToRegistry("imageStore")
+    private final InMemoryEmbeddingStore<TextSegment> imageStore = new InMemoryEmbeddingStore<>();
+
+    @BindToRegistry("imageModel")
+    private final DeterministicMediaEmbeddingModel imageModel
+            = new DeterministicMediaEmbeddingModel(16, Set.of(ContentType.IMAGE));
+
+    /** File name to error message of the deliveries the image-only pipeline refused. */
+    private final Map<String, String> refusals = new ConcurrentHashMap<>();
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                // a knowledge base reads its source: noop leaves the files where they are, and no
+                // charset is set, so the bytes reach the endpoint untouched
+                from("file:" + folder.resolve("gallery") + "?noop=true&initialDelay=0&delay=200")
+                        .routeId("gallery")
+                        .to("langchain4j-ingest:gallery?modality=media&documentIdHeader=CamelFileName"
+                            + "&embeddingStore=#bean:allStore&embeddingModel=#bean:allModel");
+
+                from("file:" + folder.resolve("photos") + "?noop=true&initialDelay=0&delay=200")
+                        .routeId("photos")
+                        // recorded and marked handled, so a refused file is not retried on every
+                        // poll while the test runs; without the handler it would be, loudly
+                        .onException(IllegalArgumentException.class).handled(true)
+                        .process(exchange -> refusals.put(
+                                exchange.getIn().getHeader(Exchange.FILE_NAME, String.class),
+                                exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Exception.class).getMessage()))
+                        .end()
+                        .to("langchain4j-ingest:photos?modality=media&documentIdHeader=CamelFileName"
+                            + "&embeddingStore=#bean:imageStore&embeddingModel=#bean:imageModel");
+            }
+        };
+    }
+
+    @Test
+    void mixedFolderIngestsIntoOneStore() throws Exception {
+        NotifyBuilder ingested = new NotifyBuilder(context).fromRoute("gallery").whenCompleted(4).create();
+        Path gallery = Files.createDirectories(folder.resolve("gallery"));
+        Files.write(gallery.resolve("photo.png"), "png".getBytes(StandardCharsets.UTF_8));
+        Files.write(gallery.resolve("song.wav"), LangChain4jIngestMediaTest.wav(50));
+        Files.write(gallery.resolve("clip.mp4"), "mp4".getBytes(StandardCharsets.UTF_8));
+        Files.write(gallery.resolve("manual.pdf"), "pdf".getBytes(StandardCharsets.UTF_8));
+
+        assertThat(ingested.matches(10, TimeUnit.SECONDS)).as("all four files ingested").isTrue();
+
+        assertThat(documentIds(allStore, allModel))
+                .containsExactlyInAnyOrder("photo.png", "song.wav", "clip.mp4", "manual.pdf");
+        assertThat(allModel.contentTypes())
+                .containsExactlyInAnyOrder(ContentType.IMAGE, ContentType.AUDIO, ContentType.VIDEO, ContentType.PDF);
+        // nothing consumed: the files are still there for the next poll, remembered by the register
+        assertThat(gallery.toFile().list()).hasSize(4);
+    }
+
+    @Test
+    void mediumTheModelLacksFailsPerFileWhileTheRestIngests() throws Exception {
+        NotifyBuilder done = new NotifyBuilder(context).fromRoute("photos").whenCompleted(3).create();
+        Path photos = Files.createDirectories(folder.resolve("photos"));
+        Files.write(photos.resolve("photo.png"), "png".getBytes(StandardCharsets.UTF_8));
+        Files.write(photos.resolve("song.wav"), LangChain4jIngestMediaTest.wav(50));
+        Files.write(photos.resolve("manual.pdf"), "pdf".getBytes(StandardCharsets.UTF_8));
+
+        assertThat(done.matches(10, TimeUnit.SECONDS)).as("all three files handled").isTrue();
+
+        // the image went in; the media the model does not declare were refused one by one
+        assertThat(documentIds(imageStore, imageModel)).containsExactly("photo.png");
+        assertThat(refusals.keySet()).containsExactlyInAnyOrder("song.wav", "manual.pdf");
+        assertThat(refusals.get("song.wav")).contains("does not support AUDIO input");
+        assertThat(refusals.get("manual.pdf")).contains("does not support PDF input");
+        // refused before the bytes were read: the model never saw them
+        assertThat(imageModel.contentTypes()).containsExactly(ContentType.IMAGE);
+    }
+
+    /** Every document id in the store; the fake model gives a query no pull towards any document. */
+    private static List<String> documentIds(
+            InMemoryEmbeddingStore<TextSegment> store,
+            DeterministicMediaEmbeddingModel model) {
+        return store.search(EmbeddingSearchRequest.builder()
+                .queryEmbedding(model.embeddingOf("any".getBytes(StandardCharsets.UTF_8)))
+                .maxResults(10)
+                .minScore(0.0)
+                .build()).matches().stream()
+                .map((EmbeddingMatch<TextSegment> match) -> match.embedded().metadata()
+                        .getString(LangChain4jIngest.METADATA_DOCUMENT_ID))
+                .toList();
+    }
+}
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestMediaTest.java b/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestMediaTest.java
new file mode 100644
index 000000000000..322d7dc22f65
--- /dev/null
+++ b/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestMediaTest.java
@@ -0,0 +1,383 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.langchain4j.ingest;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import dev.langchain4j.data.message.ContentType;
+import dev.langchain4j.data.segment.TextSegment;
+import dev.langchain4j.model.embedding.listener.EmbeddingModelListener;
+import dev.langchain4j.store.embedding.EmbeddingMatch;
+import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
+import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.Exchange;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.IdempotentRepository;
+import org.apache.camel.support.KeyValueIdempotentRepository;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.catchThrowable;
+
+class LangChain4jIngestMediaTest extends CamelTestSupport {
+
+    @BindToRegistry("store")
+    private final InMemoryEmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>();
+
+    @BindToRegistry("model")
+    private final DeterministicMediaEmbeddingModel model = new DeterministicMediaEmbeddingModel(16);
+
+    /** Claims taken on the register, so a test can assert a rejected delivery never took one. */
+    private final AtomicInteger claims = new AtomicInteger();
+
+    @BindToRegistry("register")
+    private final IdempotentRepository register = new KeyValueIdempotentRepository() {
+        @Override
+        public boolean add(String key) {
+            claims.incrementAndGet();
+            return super.add(key);
+        }
+    };
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:media")
+                        .to("langchain4j-ingest:library?modality=media&documentIdHeader=CamelFileName");
+                // case and parameters are normalised before the type reaches the model
+                from("direct:typed")
+                        .to("langchain4j-ingest:typed?modality=media&documentIdHeader=CamelFileName"
+                            + "&contentType=Image/PNG;charset=binary");
+                from("direct:capped")
+                        .to("langchain4j-ingest:capped?modality=media&documentIdHeader=CamelFileName"
+                            + "&maxDocumentSize=100");
+                from("direct:min")
+                        .to("langchain4j-ingest:min?modality=media&documentIdHeader=CamelFileName"
+                            + "&minDocumentSize=1000000");
+                from("direct:dedup")
+                        .to("langchain4j-ingest:dedup?modality=media&documentIdHeader=CamelFileName"
+                            + "&idempotentRepository=#bean:register");
+                // the batch size does not apply to media, so even an invalid value must not fail the start
+                from("direct:batch0")
+                        .to("langchain4j-ingest:batch0?modality=media&documentIdHeader=CamelFileName"
+                            + "&embeddingBatchSize=0");
+            }
+        };
+    }
+
+    @Test
+    void ingestsOneVectorPerDocumentWithIdentityMetadata() {
+        byte[] wav = wav(200);
+
+        IngestResult result = template.requestBodyAndHeader("direct:media", wav,
+                Exchange.FILE_NAME, "song-1.wav", IngestResult.class);
+
+        assertThat(result.outcome()).isEqualTo(IngestResult.Outcome.INGESTED);
+        assertThat(result.pipeline()).isEqualTo("library");
+        assertThat(result.documentId()).isEqualTo("song-1.wav");
+        assertThat(result.segmentsWritten()).isEqualTo(1);
+        assertThat(model.mimeTypes()).containsExactly("audio/wav");
+        assertThat(model.contentTypes()).containsExactly(ContentType.AUDIO);
+
+        List<EmbeddingMatch<TextSegment>> matches = search(wav);
+        assertThat(matches).hasSize(1);
+        EmbeddingMatch<TextSegment> match = matches.get(0);
+        // the same file embeds to the same vector: a query by it is an exact hit
+        assertThat(match.score()).isGreaterThan(0.99);
+        // the placeholder segment carries the id as text and the same identity stamps as text segments
+        assertThat(match.embedded().text()).isEqualTo("song-1.wav");
+        assertThat(match.embedded().metadata().getString(LangChain4jIngest.METADATA_PIPELINE)).isEqualTo("library");
+        assertThat(match.embedded().metadata().getString(LangChain4jIngest.METADATA_DOCUMENT_ID))
+                .isEqualTo("song-1.wav");
+        assertThat(match.embedded().metadata().getString(LangChain4jIngest.METADATA_CONTENT_TYPE)).isEqualTo("audio/wav");
+    }
+
+    @Test
+    void everyMediumIsTypedByItsExtension() {
+        // the bodies are fake: the bytes of the file name, not a real image or video. Nothing in
+        // the pipeline inspects the content - the medium comes from the extension and the fake
+        // model hashes whatever bytes it gets - so real fixtures would prove nothing more
+        for (String name : List.of("photo.png", "clip.mp4", "manual.pdf", "voice.opus", "recording.webm")) {
+            IngestResult result = template.requestBodyAndHeader("direct:media",
+                    name.getBytes(StandardCharsets.UTF_8), Exchange.FILE_NAME, name, IngestResult.class);
+            assertThat(result.outcome()).as(name).isEqualTo(IngestResult.Outcome.INGESTED);
+            assertThat(result.segmentsWritten()).as(name).isEqualTo(1);
+        }
+
+        // Camel's MIME table decides: an .opus file is Ogg-encapsulated, a .webm recording is video
+        assertThat(model.contentTypes()).containsExactly(
+                ContentType.IMAGE, ContentType.VIDEO, ContentType.PDF, ContentType.AUDIO, ContentType.VIDEO);
+        assertThat(model.mimeTypes())
+                .containsExactly("image/png", "video/mp4", "application/pdf", "audio/ogg", "video/webm");
+    }
+
+    @Test
+    void emptyBodyAnswersEmpty() {
+        IngestResult result = template.requestBodyAndHeader("direct:media", new byte[0],
+                Exchange.FILE_NAME, "silence.wav", IngestResult.class);
+
+        assertThat(result.outcome()).isEqualTo(IngestResult.Outcome.EMPTY);
+        assertThat(result.segmentsWritten()).isZero();
+    }
+
+    @Test
+    void nullBodyAnswersEmptyButAnUnconvertibleOneFails() {
+        IngestResult result = template.requestBodyAndHeader("direct:media", null,
+                Exchange.FILE_NAME, "missing.wav", IngestResult.class);
+        assertThat(result.outcome()).isEqualTo(IngestResult.Outcome.EMPTY);
+
+        // a POJO has no converter to bytes: a silent null conversion would answer EMPTY and
+        // release the claim, hiding a wiring mistake - it must fail instead
+        assertThatThrownBy(() -> template.requestBodyAndHeader("direct:media", new Object(),
+                Exchange.FILE_NAME, "pojo.wav"))
+                .isInstanceOf(CamelExecutionException.class)
+                .hasStackTraceContaining("InvalidPayloadException");
+    }
+
+    @Test
+    void tooSmallMediaIsFiltered() {
+        IngestResult result = template.requestBodyAndHeader("direct:min", wav(50),
+                Exchange.FILE_NAME, "tiny.wav", IngestResult.class);
+
+        assertThat(result.outcome()).isEqualTo(IngestResult.Outcome.FILTERED);
+    }
+
+    @Test
+    void oversizedMediaFailsAndTheCapCountsBytes() {
+        assertThatThrownBy(() -> template.requestBodyAndHeader("direct:capped", wav(200),
+                Exchange.FILE_NAME, "big.wav"))
+                .isInstanceOf(CamelExecutionException.class)
+                .hasStackTraceContaining("exceeds maxDocumentSize")
+                .hasStackTraceContaining("bytes");
+    }
+
+    @Test
+    void oversizedFileIsRefusedBeforeItIsRead() {
+        // a file consumer announces the length up front; the cap applies to it before any read
+        Throwable thrown = catchThrowable(() -> template.requestBodyAndHeaders("direct:capped", unreadableBody(),
+                Map.<String, Object> of(Exchange.FILE_NAME, "big.wav", Exchange.FILE_LENGTH, 101L)));
+
+        assertThat(thrown).isInstanceOf(CamelExecutionException.class)
+                .hasStackTraceContaining("exceeds maxDocumentSize (101 > 100 bytes)");
+        assertThat(stackTraceOf(thrown)).as("the body must not have been read").doesNotContain("body must not be read");
+    }
+
+    @Test
+    void mimeTypeComesFromTheExtensionOrTheNormalisedOption() {
+        // the same WAV bytes travel under every name below: only the name, or the option, decides
+        // the type, never the content
+        byte[] clip = wav(50);
+
+        template.requestBodyAndHeader("direct:media", clip, Exchange.FILE_NAME, "clip.mp3", IngestResult.class);
+        template.requestBodyAndHeader("direct:typed", clip, Exchange.FILE_NAME, "clip.wav", IngestResult.class);
+
+        // the option wins over the extension, lower-cased and without its parameter
+        assertThat(model.mimeTypes()).containsExactly("audio/mpeg", "image/png");
+        assertThat(model.contentTypes()).containsExactly(ContentType.AUDIO, ContentType.IMAGE);
+
+        assertThatThrownBy(() -> template.requestBodyAndHeader("direct:media", clip,
+                Exchange.FILE_NAME, "clip.zzz"))
+                .isInstanceOf(CamelExecutionException.class)
+                .hasStackTraceContaining("cannot tell the media type")
+                .hasStackTraceContaining("contentType");
+        // a known type that is no medium: Camel's table knows .txt, the pipeline cannot embed it
+        assertThatThrownBy(() -> template.requestBodyAndHeader("direct:media", clip,
+                Exchange.FILE_NAME, "notes.txt"))
+                .isInstanceOf(CamelExecutionException.class)
+                .hasStackTraceContaining("is not a media type");
+        // a bare id without extension is not typed by its name, even one that reads like an extension
+        assertThatThrownBy(() -> template.requestBodyAndHeader("direct:media", clip,
+                Exchange.FILE_NAME, "mp3"))
+                .isInstanceOf(CamelExecutionException.class)
+                .hasStackTraceContaining("cannot tell the media type");
+    }
+
+    @Test
+    void duplicateIdIsSkippedWithARepository() {
+        byte[] wav = wav(50);
+
+        IngestResult first = template.requestBodyAndHeader("direct:dedup", wav,
+                Exchange.FILE_NAME, "song-2.wav", IngestResult.class);
+        IngestResult second = template.requestBodyAndHeader("direct:dedup", wav,
+                Exchange.FILE_NAME, "song-2.wav", IngestResult.class);
+
+        assertThat(first.outcome()).isEqualTo(IngestResult.Outcome.INGESTED);
+        assertThat(second.outcome()).isEqualTo(IngestResult.Outcome.SKIPPED);
+        assertThat(search(wav)).hasSize(1);
+    }
+
+    @Test
+    void unknownTypeFailsBeforeTheClaimAndTheBodyRead() {
+        int claimsBefore = claims.get();
+
+        Throwable thrown = catchThrowable(() -> template.requestBodyAndHeader("direct:dedup", unreadableBody(),
+                Exchange.FILE_NAME, "clip.zzz"));
+
+        assertThat(thrown).isInstanceOf(CamelExecutionException.class)
+                .hasStackTraceContaining("cannot tell the media type");
+        assertThat(stackTraceOf(thrown)).as("the body must not have been read").doesNotContain("body must not be read");
+        assertThat(claims.get()).as("no claim for a document whose type cannot be told").isEqualTo(claimsBefore);
+    }
+
+    @Test
+    void mediumTheModelLacksFailsBeforeTheBodyRead() throws Exception {
+        try (DefaultCamelContext context = new DefaultCamelContext()) {
+            context.getRegistry().bind("store", new InMemoryEmbeddingStore<TextSegment>());
+            context.getRegistry().bind("model", new DeterministicMediaEmbeddingModel(16, Set.of(ContentType.AUDIO)));
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("direct:in").to("langchain4j-ingest:pipe?modality=media&documentIdHeader=CamelFileName");
+                }
+            });
+            context.start();
+            ProducerTemplate producer = context.createProducerTemplate();
+
+            Throwable thrown = catchThrowable(() -> producer.requestBodyAndHeader("direct:in", unreadableBody(),
+                    Exchange.FILE_NAME, "photo.png"));
+
+            assertThat(thrown).isInstanceOf(CamelExecutionException.class)
+                    .hasStackTraceContaining("does not support IMAGE input")
+                    .hasStackTraceContaining("photo.png");
+            assertThat(stackTraceOf(thrown)).as("the body must not have been read")
+                    .doesNotContain("body must not be read");
+        }
+    }
+
+    @Test
+    void batchSizeIsNotValidatedInMediaMode() {
+        IngestResult result = template.requestBodyAndHeader("direct:batch0", wav(50),
+                Exchange.FILE_NAME, "song-3.wav", IngestResult.class);
+
+        assertThat(result.outcome()).isEqualTo(IngestResult.Outcome.INGESTED);
+    }
+
+    @Test
+    void textOnlyModelFailsTheStart() throws Exception {
+        assertStartFails(new DeterministicEmbeddingModel(16), "langchain4j-ingest:pipe?modality=media",
+                "does not support any media input", "supportedContentTypes");
+    }
+
+    @Test
+    void modelWithoutContentTypesFailsTheStart() throws Exception {
+        assertStartFails(new DeterministicEmbeddingModel(16) {
+            @Override
+            public Set<ContentType> supportedContentTypes() {
+                return null;
+            }
+        }, "langchain4j-ingest:pipe?modality=media", "does not support any media input", "are null");
+    }
+
+    @Test
+    void contentTypeTheModelLacksFailsTheStart() throws Exception {
+        assertStartFails(new DeterministicMediaEmbeddingModel(16, Set.of(ContentType.AUDIO)),
+                "langchain4j-ingest:pipe?modality=media&contentType=image/png",
+                "does not support IMAGE input", "contentType option");
+    }
+
+    @Test
+    void listenerWrappedModelFailsTheStart() throws Exception {
+        // LangChain4j wraps the model in a ListeningEmbeddingModel, which builds the listener
+        // context from the input text and would fail every media request with an opaque error
+        assertStartFails(new DeterministicMediaEmbeddingModel(16).addListener(new EmbeddingModelListener() {
+        }), "langchain4j-ingest:pipe?modality=media", "EmbeddingModelListeners attached");
+    }
+
+    private static void assertStartFails(Object model, String uri, String... messageFragments) throws Exception {
+        try (DefaultCamelContext context = new DefaultCamelContext()) {
+            context.getRegistry().bind("store", new InMemoryEmbeddingStore<TextSegment>());
+            context.getRegistry().bind("model", model);
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("direct:in").to(uri);
+                }
+            });
+
+            var assertion = assertThatThrownBy(context::start);
+            for (String fragment : messageFragments) {
+                assertion.hasStackTraceContaining(fragment);
+            }
+        }
+    }
+
+    private List<EmbeddingMatch<TextSegment>> search(byte[] bytes) {
+        return store.search(EmbeddingSearchRequest.builder()
+                .queryEmbedding(model.embeddingOf(bytes))
+                .maxResults(10)
+                .build()).matches();
+    }
+
+    /** A body whose read fails loudly, so a test can prove the type was rejected before any read. */
+    private static InputStream unreadableBody() {
+        return new InputStream() {
+            @Override
+            public int read() throws IOException {
+                throw new IOException("body must not be read");
+            }
+        };
+    }
+
+    private static String stackTraceOf(Throwable thrown) {
+        StringWriter trace = new StringWriter();
+        thrown.printStackTrace(new PrintWriter(trace));
+        return trace.toString();
+    }
+
+    /**
+     * A mono 16 kHz 16-bit PCM WAV of the given length holding a 440 Hz tone, its RIFF header written by hand: a real
+     * file with no binary fixture, and no java.desktop module, which a headless JDK may lack.
+     */
+    static byte[] wav(int millis) {
+        int sampleRate = 16_000;
+        int frames = sampleRate * millis / 1000;
+        int dataSize = frames * 2;
+        ByteBuffer buffer = ByteBuffer.allocate(44 + dataSize).order(ByteOrder.LITTLE_ENDIAN);
+        buffer.put("RIFF".getBytes(StandardCharsets.US_ASCII)).putInt(36 + dataSize)
+                .put("WAVE".getBytes(StandardCharsets.US_ASCII))
+                .put("fmt ".getBytes(StandardCharsets.US_ASCII)).putInt(16)
+                .putShort((short) 1) // PCM
+                .putShort((short) 1) // mono
+                .putInt(sampleRate)
+                .putInt(sampleRate * 2) // byte rate
+                .putShort((short) 2) // block align
+                .putShort((short) 16) // bits per sample
+                .put("data".getBytes(StandardCharsets.US_ASCII)).putInt(dataSize);
+        for (int i = 0; i < frames; i++) {
+            buffer.putShort((short) (Math.sin(2 * Math.PI * 440 * i / sampleRate) * Short.MAX_VALUE / 4));
+        }
+        return buffer.array();
+    }
+}
diff --git a/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestValidationTest.java b/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestValidationTest.java
index 310a4d4dcbf9..2d668e5e3eb0 100644
--- a/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestValidationTest.java
+++ b/components/camel-ai/camel-langchain4j-ingest/src/test/java/org/apache/camel/component/langchain4j/ingest/LangChain4jIngestValidationTest.java
@@ -16,6 +16,9 @@
  */
 package org.apache.camel.component.langchain4j.ingest;
 
+import java.util.List;
+
+import dev.langchain4j.data.document.DocumentSplitter;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.junit.jupiter.api.Test;
@@ -126,6 +129,52 @@ class LangChain4jIngestValidationTest {
         assertBoundsRejected("langchain4j-ingest:pipe?maxSegmentSize=100&maxOverlapSize=100");
     }
 
+    @Test
+    void contentTypeWithTextModalityFailsTheStart() throws Exception {
+        try (DefaultCamelContext context = new DefaultCamelContext()) {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("direct:in").to("langchain4j-ingest:pipe?contentType=audio/wav");
+                }
+            });
+
+            assertThatThrownBy(context::start)
+                    .hasStackTraceContaining("contentType only applies to modality=media");
+        }
+    }
+
+    @Test
+    void documentSplitterWithMediaModalityFailsTheStart() throws Exception {
+        try (DefaultCamelContext context = new DefaultCamelContext()) {
+            context.getRegistry().bind("splitter", (DocumentSplitter) document -> List.of());
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("direct:in").to("langchain4j-ingest:pipe?modality=media&documentSplitter=#bean:splitter");
+                }
+            });
+
+            assertThatThrownBy(context::start)
+                    .hasStackTraceContaining("documentSplitter does not apply to modality=media");
+        }
+    }
+
+    @Test
+    void unknownModalityFailsTheStart() throws Exception {
+        try (DefaultCamelContext context = new DefaultCamelContext()) {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("direct:in").to("langchain4j-ingest:pipe?modality=video");
+                }
+            });
+
+            assertThatThrownBy(context::start)
+                    .hasStackTraceContaining("video");
+        }
+    }
+
     private static void assertBoundsRejected(String uri) throws Exception {
         try (DefaultCamelContext context = new DefaultCamelContext()) {
             context.addRoutes(new RouteBuilder() {
diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/Langchain4jIngestComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/Langchain4jIngestComponentBuilderFactory.java
index 5d2c81e2d0b0..5d137833c380 100644
--- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/Langchain4jIngestComponentBuilderFactory.java
+++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/Langchain4jIngestComponentBuilderFactory.java
@@ -68,6 +68,32 @@ public interface Langchain4jIngestComponentBuilderFactory {
             return this;
         }
     
+        /**
+         * MIME type of a media body, such as audio/wav or image/png, handed to
+         * the embedding model; matched case-insensitively, parameters after a
+         * semicolon are dropped. When not set, it is derived from the document
+         * id's file extension through Camel's own MIME table (the x- audio
+         * variants rewritten to their registered form): wav, mp3, flac, ogg,
+         * opus, m4a, aac, aiff, png, jpg, gif, webp, mp4, mov, webm (video),
+         * pdf and every other extension the table knows. A document whose type
+         * cannot be determined, whose type is no medium (text/plain, say), or
+         * whose medium the model does not declare, fails the exchange before
+         * its dedup claim and before its body is read. Only valid with
+         * modality=media - with modality=text the endpoint refuses to start,
+         * the option being a sign that modality=media was forgotten.
+         * 
+         * The option is a: &lt;code&gt;java.lang.String&lt;/code&gt; type.
+         * 
+         * Group: producer
+         * 
+         * @param contentType the value to set
+         * @return the dsl builder
+         */
+        default Langchain4jIngestComponentBuilder contentType(java.lang.String contentType) {
+            doSetProperty("contentType", contentType);
+            return this;
+        }
+    
         
         /**
          * Name of the header carrying the document id, such as CamelAwsS3Key
@@ -209,6 +235,32 @@ public interface Langchain4jIngestComponentBuilderFactory {
         }
     
         
+        /**
+         * What the message body is. text, the default, is read as a String,
+         * split into segments and embedded segment by segment. media is read as
+         * bytes and embedded whole, as one vector: audio, an image, video or a
+         * PDF, told apart by the MIME type, each needing an embedding model
+         * whose supportedContentTypes() include the matching type - the
+         * endpoint refuses to start with a text-only model. In media mode the
+         * splitter options and embeddingBatchSize do not apply,
+         * documentSplitter must not be set, and maxDocumentSize and
+         * minDocumentSize count bytes.
+         * 
+         * The option is a:
+         * &lt;code&gt;org.apache.camel.component.langchain4j.ingest.IngestModality&lt;/code&gt; type.
+         * 
+         * Default: TEXT
+         * Group: producer
+         * 
+         * @param modality the value to set
+         * @return the dsl builder
+         */
+        default Langchain4jIngestComponentBuilder modality(org.apache.camel.component.langchain4j.ingest.IngestModality modality) {
+            doSetProperty("modality", modality);
+            return this;
+        }
+    
+        
         /**
          * Whether autowiring is enabled. This is used for automatic autowiring
          * options (the option must be marked as autowired) by looking up in the
@@ -283,12 +335,14 @@ public interface Langchain4jIngestComponentBuilderFactory {
         }
     
         /**
-         * Maximum size of one document in characters, applied to the text about
-         * to be split; 0, the default, means no limit. The pipeline holds a
-         * document in memory whole, so the cap is the protection against
-         * oversized - on a consumer-fed pipeline, attacker-sized - payloads. An
-         * oversized document fails the exchange cleanly and, with a repository
-         * configured, releases its dedup claim.
+         * Maximum size of one document: characters of the text about to be
+         * split, or bytes of a media body with modality=media; 0, the default,
+         * means no limit. The pipeline holds a document in memory whole, so the
+         * cap is the protection against oversized - on a consumer-fed pipeline,
+         * attacker-sized - payloads. An oversized document fails the exchange
+         * cleanly and, with a repository configured, releases its dedup claim.
+         * With modality=media the size a file consumer announces in
+         * CamelFileLength is checked before the body is read.
          * 
          * The option is a: &lt;code&gt;int&lt;/code&gt; type.
          * 
@@ -307,9 +361,10 @@ public interface Langchain4jIngestComponentBuilderFactory {
          * #bean:name and evaluated with the message body available. A rejected
          * delivery is answered with a filtered result and releases its dedup
          * claim. Runs after the id patterns and after the dedup claim, so a
-         * duplicate is answered skipped without the filter being evaluated. Not
-         * looked up by type on purpose - an application may hold unrelated
-         * predicates.
+         * duplicate is answered skipped without the filter being evaluated. The
+         * body is still as the consumer delivered it, a file or stream say, not
+         * yet read as text or bytes. Not looked up by type on purpose - an
+         * application may hold unrelated predicates.
          * 
          * The option is a: &lt;code&gt;org.apache.camel.Predicate&lt;/code&gt;
          * type.
@@ -366,10 +421,11 @@ public interface Langchain4jIngestComponentBuilderFactory {
         }
     
         /**
-         * Minimum size of one document in characters; 0, the default, means no
-         * minimum. A shorter document - boilerplate too small to carry
-         * retrievable content - is answered with a filtered result instead of
-         * being written, and releases its dedup claim like a blank one.
+         * Minimum size of one document in characters (bytes with
+         * modality=media); 0, the default, means no minimum. A shorter document
+         * - boilerplate too small to carry retrievable content - is answered
+         * with a filtered result instead of being written, and releases its
+         * dedup claim like a blank one.
          * 
          * The option is a: &lt;code&gt;int&lt;/code&gt; type.
          * 
@@ -404,6 +460,7 @@ public interface Langchain4jIngestComponentBuilderFactory {
                 Object value) {
             switch (name) {
             case "configuration": ((LangChain4jIngestComponent) component).setConfiguration((org.apache.camel.component.langchain4j.ingest.LangChain4jIngestConfiguration) value); return true;
+            case "contentType": getOrCreateConfiguration((LangChain4jIngestComponent) component).setContentType((java.lang.String) value); return true;
             case "documentIdHeader": getOrCreateConfiguration((LangChain4jIngestComponent) component).setDocumentIdHeader((java.lang.String) value); return true;
             case "embeddingBatchSize": getOrCreateConfiguration((LangChain4jIngestComponent) component).setEmbeddingBatchSize((int) value); return true;
             case "embeddingModel": getOrCreateConfiguration((LangChain4jIngestComponent) component).setEmbeddingModel((dev.langchain4j.model.embedding.EmbeddingModel) value); return true;
@@ -411,6 +468,7 @@ public interface Langchain4jIngestComponentBuilderFactory {
             case "lazyStartProducer": ((LangChain4jIngestComponent) component).setLazyStartProducer((boolean) value); return true;
             case "maxOverlapSize": getOrCreateConfiguration((LangChain4jIngestComponent) component).setMaxOverlapSize((int) value); return true;
             case "maxSegmentSize": getOrCreateConfiguration((LangChain4jIngestComponent) component).setMaxSegmentSize((int) value); return true;
+            case "modality": getOrCreateConfiguration((LangChain4jIngestComponent) component).setModality((org.apache.camel.component.langchain4j.ingest.IngestModality) value); return true;
             case "autowiredEnabled": ((LangChain4jIngestComponent) component).setAutowiredEnabled((boolean) value); return true;
             case "documentSplitter": getOrCreateConfiguration((LangChain4jIngestComponent) component).setDocumentSplitter((dev.langchain4j.data.document.DocumentSplitter) value); return true;
             case "idempotentRepository": getOrCreateConfiguration((LangChain4jIngestComponent) component).setIdempotentRepository((org.apache.camel.spi.IdempotentRepository) value); return true;
diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jIngestEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jIngestEndpointBuilderFactory.java
index fe09a62fb394..e39747d4f660 100644
--- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jIngestEndpointBuilderFactory.java
+++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jIngestEndpointBuilderFactory.java
@@ -45,6 +45,31 @@ public interface LangChain4jIngestEndpointBuilderFactory {
             return (AdvancedLangChain4jIngestEndpointBuilder) this;
         }
 
+        /**
+         * MIME type of a media body, such as audio/wav or image/png, handed to
+         * the embedding model; matched case-insensitively, parameters after a
+         * semicolon are dropped. When not set, it is derived from the document
+         * id's file extension through Camel's own MIME table (the x- audio
+         * variants rewritten to their registered form): wav, mp3, flac, ogg,
+         * opus, m4a, aac, aiff, png, jpg, gif, webp, mp4, mov, webm (video),
+         * pdf and every other extension the table knows. A document whose type
+         * cannot be determined, whose type is no medium (text/plain, say), or
+         * whose medium the model does not declare, fails the exchange before
+         * its dedup claim and before its body is read. Only valid with
+         * modality=media - with modality=text the endpoint refuses to start,
+         * the option being a sign that modality=media was forgotten.
+         * 
+         * The option is a: <code>java.lang.String</code> type.
+         * 
+         * Group: producer
+         * 
+         * @param contentType the value to set
+         * @return the dsl builder
+         */
+        default LangChain4jIngestEndpointBuilder contentType(String contentType) {
+            doSetProperty("contentType", contentType);
+            return this;
+        }
         /**
          * Name of the header carrying the document id, such as CamelAwsS3Key
          * for an S3 consumer or CamelKafkaKey for a Kafka one. The
@@ -235,14 +260,63 @@ public interface LangChain4jIngestEndpointBuilderFactory {
             doSetProperty("maxSegmentSize", maxSegmentSize);
             return this;
         }
+        /**
+         * What the message body is. text, the default, is read as a String,
+         * split into segments and embedded segment by segment. media is read as
+         * bytes and embedded whole, as one vector: audio, an image, video or a
+         * PDF, told apart by the MIME type, each needing an embedding model
+         * whose supportedContentTypes() include the matching type - the
+         * endpoint refuses to start with a text-only model. In media mode the
+         * splitter options and embeddingBatchSize do not apply,
+         * documentSplitter must not be set, and maxDocumentSize and
+         * minDocumentSize count bytes.
+         * 
+         * The option is a:
+         * <code>org.apache.camel.component.langchain4j.ingest.IngestModality</code> type.
+         * 
+         * Default: TEXT
+         * Group: producer
+         * 
+         * @param modality the value to set
+         * @return the dsl builder
+         */
+        default LangChain4jIngestEndpointBuilder modality(org.apache.camel.component.langchain4j.ingest.IngestModality modality) {
+            doSetProperty("modality", modality);
+            return this;
+        }
+        /**
+         * What the message body is. text, the default, is read as a String,
+         * split into segments and embedded segment by segment. media is read as
+         * bytes and embedded whole, as one vector: audio, an image, video or a
+         * PDF, told apart by the MIME type, each needing an embedding model
+         * whose supportedContentTypes() include the matching type - the
+         * endpoint refuses to start with a text-only model. In media mode the
+         * splitter options and embeddingBatchSize do not apply,
+         * documentSplitter must not be set, and maxDocumentSize and
+         * minDocumentSize count bytes.
+         * 
+         * The option will be converted to a
+         * <code>org.apache.camel.component.langchain4j.ingest.IngestModality</code> type.
+         * 
+         * Default: TEXT
+         * Group: producer
+         * 
+         * @param modality the value to set
+         * @return the dsl builder
+         */
+        default LangChain4jIngestEndpointBuilder modality(String modality) {
+            doSetProperty("modality", modality);
+            return this;
+        }
         /**
          * A Predicate deciding whether a delivery is ingested, referenced as
          * #bean:name and evaluated with the message body available. A rejected
          * delivery is answered with a filtered result and releases its dedup
          * claim. Runs after the id patterns and after the dedup claim, so a
-         * duplicate is answered skipped without the filter being evaluated. Not
-         * looked up by type on purpose - an application may hold unrelated
-         * predicates.
+         * duplicate is answered skipped without the filter being evaluated. The
+         * body is still as the consumer delivered it, a file or stream say, not
+         * yet read as text or bytes. Not looked up by type on purpose - an
+         * application may hold unrelated predicates.
          * 
          * The option is a: <code>org.apache.camel.Predicate</code> type.
          * 
@@ -260,9 +334,10 @@ public interface LangChain4jIngestEndpointBuilderFactory {
          * #bean:name and evaluated with the message body available. A rejected
          * delivery is answered with a filtered result and releases its dedup
          * claim. Runs after the id patterns and after the dedup claim, so a
-         * duplicate is answered skipped without the filter being evaluated. Not
-         * looked up by type on purpose - an application may hold unrelated
-         * predicates.
+         * duplicate is answered skipped without the filter being evaluated. The
+         * body is still as the consumer delivered it, a file or stream say, not
+         * yet read as text or bytes. Not looked up by type on purpose - an
+         * application may hold unrelated predicates.
          * 
          * The option will be converted to a
          * <code>org.apache.camel.Predicate</code> type.
@@ -316,10 +391,11 @@ public interface LangChain4jIngestEndpointBuilderFactory {
             return this;
         }
         /**
-         * Minimum size of one document in characters; 0, the default, means no
-         * minimum. A shorter document - boilerplate too small to carry
-         * retrievable content - is answered with a filtered result instead of
-         * being written, and releases its dedup claim like a blank one.
+         * Minimum size of one document in characters (bytes with
+         * modality=media); 0, the default, means no minimum. A shorter document
+         * - boilerplate too small to carry retrievable content - is answered
+         * with a filtered result instead of being written, and releases its
+         * dedup claim like a blank one.
          * 
          * The option is a: <code>int</code> type.
          * 
@@ -334,10 +410,11 @@ public interface LangChain4jIngestEndpointBuilderFactory {
             return this;
         }
         /**
-         * Minimum size of one document in characters; 0, the default, means no
-         * minimum. A shorter document - boilerplate too small to carry
-         * retrievable content - is answered with a filtered result instead of
-         * being written, and releases its dedup claim like a blank one.
+         * Minimum size of one document in characters (bytes with
+         * modality=media); 0, the default, means no minimum. A shorter document
+         * - boilerplate too small to carry retrievable content - is answered
+         * with a filtered result instead of being written, and releases its
+         * dedup claim like a blank one.
          * 
          * The option will be converted to a <code>int</code> type.
          * 
@@ -508,12 +585,14 @@ public interface LangChain4jIngestEndpointBuilderFactory {
             return this;
         }
         /**
-         * Maximum size of one document in characters, applied to the text about
-         * to be split; 0, the default, means no limit. The pipeline holds a
-         * document in memory whole, so the cap is the protection against
-         * oversized - on a consumer-fed pipeline, attacker-sized - payloads. An
-         * oversized document fails the exchange cleanly and, with a repository
-         * configured, releases its dedup claim.
+         * Maximum size of one document: characters of the text about to be
+         * split, or bytes of a media body with modality=media; 0, the default,
+         * means no limit. The pipeline holds a document in memory whole, so the
+         * cap is the protection against oversized - on a consumer-fed pipeline,
+         * attacker-sized - payloads. An oversized document fails the exchange
+         * cleanly and, with a repository configured, releases its dedup claim.
+         * With modality=media the size a file consumer announces in
+         * CamelFileLength is checked before the body is read.
          * 
          * The option is a: <code>int</code> type.
          * 
@@ -528,12 +607,14 @@ public interface LangChain4jIngestEndpointBuilderFactory {
             return this;
         }
         /**
-         * Maximum size of one document in characters, applied to the text about
-         * to be split; 0, the default, means no limit. The pipeline holds a
-         * document in memory whole, so the cap is the protection against
-         * oversized - on a consumer-fed pipeline, attacker-sized - payloads. An
-         * oversized document fails the exchange cleanly and, with a repository
-         * configured, releases its dedup claim.
+         * Maximum size of one document: characters of the text about to be
+         * split, or bytes of a media body with modality=media; 0, the default,
+         * means no limit. The pipeline holds a document in memory whole, so the
+         * cap is the protection against oversized - on a consumer-fed pipeline,
+         * attacker-sized - payloads. An oversized document fails the exchange
+         * cleanly and, with a repository configured, releases its dedup claim.
+         * With modality=media the size a file consumer announces in
+         * CamelFileLength is checked before the body is read.
          * 
          * The option will be converted to a <code>int</code> type.
          *