⭐
Log in to mark this repository.
describe to rknn
9540102 · gmorell · 2026-05-09 00:26
Files changed
modified
Cargo.lock
+188
−0
@@ -1322,6 +1322,7 @@ dependencies = [
| 1322 | 1322 | "minijinja-embed", |
| 1323 | 1323 | "ocrs", |
| 1324 | 1324 | "qrcode", |
| 1325 | + "reqwest", | |
| 1325 | 1326 | "rten", |
| 1326 | 1327 | "rten-imageproc", |
| 1327 | 1328 | "sea-orm", |
@@ -1708,8 +1709,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
| 1708 | 1709 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" |
| 1709 | 1710 | dependencies = [ |
| 1710 | 1711 | "cfg-if", |
| 1712 | + "js-sys", | |
| 1711 | 1713 | "libc", |
| 1712 | 1714 | "wasi 0.11.1+wasi-snapshot-preview1", |
| 1715 | + "wasm-bindgen", | |
| 1713 | 1716 | ] |
| 1714 | 1717 | |
| 1715 | 1718 | [[package]] |
@@ -1719,9 +1722,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
| 1719 | 1722 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" |
| 1720 | 1723 | dependencies = [ |
| 1721 | 1724 | "cfg-if", |
| 1725 | + "js-sys", | |
| 1722 | 1726 | "libc", |
| 1723 | 1727 | "r-efi", |
| 1724 | 1728 | "wasi 0.14.7+wasi-0.2.4", |
| 1729 | + "wasm-bindgen", | |
| 1725 | 1730 | ] |
| 1726 | 1731 | |
| 1727 | 1732 | [[package]] |
@@ -1941,6 +1946,23 @@ dependencies = [
| 1941 | 1946 | "pin-utils", |
| 1942 | 1947 | "smallvec", |
| 1943 | 1948 | "tokio", |
| 1949 | + "want", | |
| 1950 | +] | |
| 1951 | + | |
| 1952 | +[[package]] | |
| 1953 | +name = "hyper-rustls" | |
| 1954 | +version = "0.27.9" | |
| 1955 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1956 | +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" | |
| 1957 | +dependencies = [ | |
| 1958 | + "http", | |
| 1959 | + "hyper", | |
| 1960 | + "hyper-util", | |
| 1961 | + "rustls", | |
| 1962 | + "tokio", | |
| 1963 | + "tokio-rustls", | |
| 1964 | + "tower-service", | |
| 1965 | + "webpki-roots 1.0.2", | |
| 1944 | 1966 | ] |
| 1945 | 1967 | |
| 1946 | 1968 | [[package]] |
@@ -1949,14 +1971,22 @@ version = "0.1.17"
| 1949 | 1971 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1950 | 1972 | checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" |
| 1951 | 1973 | dependencies = [ |
| 1974 | + "base64", | |
| 1952 | 1975 | "bytes", |
| 1976 | + "futures-channel", | |
| 1953 | 1977 | "futures-core", |
| 1978 | + "futures-util", | |
| 1954 | 1979 | "http", |
| 1955 | 1980 | "http-body", |
| 1956 | 1981 | "hyper", |
| 1982 | + "ipnet", | |
| 1983 | + "libc", | |
| 1984 | + "percent-encoding", | |
| 1957 | 1985 | "pin-project-lite", |
| 1986 | + "socket2 0.6.0", | |
| 1958 | 1987 | "tokio", |
| 1959 | 1988 | "tower-service", |
| 1989 | + "tracing", | |
| 1960 | 1990 | ] |
| 1961 | 1991 | |
| 1962 | 1992 | [[package]] |
@@ -2209,6 +2239,22 @@ dependencies = [
| 2209 | 2239 | "libc", |
| 2210 | 2240 | ] |
| 2211 | 2241 | |
| 2242 | +[[package]] | |
| 2243 | +name = "ipnet" | |
| 2244 | +version = "2.12.0" | |
| 2245 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2246 | +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" | |
| 2247 | + | |
| 2248 | +[[package]] | |
| 2249 | +name = "iri-string" | |
| 2250 | +version = "0.7.12" | |
| 2251 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2252 | +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" | |
| 2253 | +dependencies = [ | |
| 2254 | + "memchr", | |
| 2255 | + "serde", | |
| 2256 | +] | |
| 2257 | + | |
| 2212 | 2258 | [[package]] |
| 2213 | 2259 | name = "is_terminal_polyfill" |
| 2214 | 2260 | version = "1.70.1" |
@@ -2421,6 +2467,12 @@ dependencies = [
| 2421 | 2467 | "imgref", |
| 2422 | 2468 | ] |
| 2423 | 2469 | |
| 2470 | +[[package]] | |
| 2471 | +name = "lru-slab" | |
| 2472 | +version = "0.1.2" | |
| 2473 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2474 | +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" | |
| 2475 | + | |
| 2424 | 2476 | [[package]] |
| 2425 | 2477 | name = "mach2" |
| 2426 | 2478 | version = "0.4.3" |
@@ -3287,6 +3339,61 @@ version = "2.0.1"
| 3287 | 3339 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3288 | 3340 | checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" |
| 3289 | 3341 | |
| 3342 | +[[package]] | |
| 3343 | +name = "quinn" | |
| 3344 | +version = "0.11.9" | |
| 3345 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3346 | +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" | |
| 3347 | +dependencies = [ | |
| 3348 | + "bytes", | |
| 3349 | + "cfg_aliases", | |
| 3350 | + "pin-project-lite", | |
| 3351 | + "quinn-proto", | |
| 3352 | + "quinn-udp", | |
| 3353 | + "rustc-hash 2.1.2", | |
| 3354 | + "rustls", | |
| 3355 | + "socket2 0.6.0", | |
| 3356 | + "thiserror", | |
| 3357 | + "tokio", | |
| 3358 | + "tracing", | |
| 3359 | + "web-time", | |
| 3360 | +] | |
| 3361 | + | |
| 3362 | +[[package]] | |
| 3363 | +name = "quinn-proto" | |
| 3364 | +version = "0.11.14" | |
| 3365 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3366 | +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" | |
| 3367 | +dependencies = [ | |
| 3368 | + "bytes", | |
| 3369 | + "getrandom 0.3.3", | |
| 3370 | + "lru-slab", | |
| 3371 | + "rand 0.9.2", | |
| 3372 | + "ring", | |
| 3373 | + "rustc-hash 2.1.2", | |
| 3374 | + "rustls", | |
| 3375 | + "rustls-pki-types", | |
| 3376 | + "slab", | |
| 3377 | + "thiserror", | |
| 3378 | + "tinyvec", | |
| 3379 | + "tracing", | |
| 3380 | + "web-time", | |
| 3381 | +] | |
| 3382 | + | |
| 3383 | +[[package]] | |
| 3384 | +name = "quinn-udp" | |
| 3385 | +version = "0.5.14" | |
| 3386 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3387 | +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" | |
| 3388 | +dependencies = [ | |
| 3389 | + "cfg_aliases", | |
| 3390 | + "libc", | |
| 3391 | + "once_cell", | |
| 3392 | + "socket2 0.6.0", | |
| 3393 | + "tracing", | |
| 3394 | + "windows-sys 0.60.2", | |
| 3395 | +] | |
| 3396 | + | |
| 3290 | 3397 | [[package]] |
| 3291 | 3398 | name = "quote" |
| 3292 | 3399 | version = "1.0.41" |
@@ -3522,6 +3629,46 @@ dependencies = [
| 3522 | 3629 | "bytecheck", |
| 3523 | 3630 | ] |
| 3524 | 3631 | |
| 3632 | +[[package]] | |
| 3633 | +name = "reqwest" | |
| 3634 | +version = "0.12.24" | |
| 3635 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3636 | +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" | |
| 3637 | +dependencies = [ | |
| 3638 | + "base64", | |
| 3639 | + "bytes", | |
| 3640 | + "futures-core", | |
| 3641 | + "futures-util", | |
| 3642 | + "http", | |
| 3643 | + "http-body", | |
| 3644 | + "http-body-util", | |
| 3645 | + "hyper", | |
| 3646 | + "hyper-rustls", | |
| 3647 | + "hyper-util", | |
| 3648 | + "js-sys", | |
| 3649 | + "log", | |
| 3650 | + "mime_guess", | |
| 3651 | + "percent-encoding", | |
| 3652 | + "pin-project-lite", | |
| 3653 | + "quinn", | |
| 3654 | + "rustls", | |
| 3655 | + "rustls-pki-types", | |
| 3656 | + "serde", | |
| 3657 | + "serde_json", | |
| 3658 | + "serde_urlencoded", | |
| 3659 | + "sync_wrapper", | |
| 3660 | + "tokio", | |
| 3661 | + "tokio-rustls", | |
| 3662 | + "tower", | |
| 3663 | + "tower-http", | |
| 3664 | + "tower-service", | |
| 3665 | + "url", | |
| 3666 | + "wasm-bindgen", | |
| 3667 | + "wasm-bindgen-futures", | |
| 3668 | + "web-sys", | |
| 3669 | + "webpki-roots 1.0.2", | |
| 3670 | +] | |
| 3671 | + | |
| 3525 | 3672 | [[package]] |
| 3526 | 3673 | name = "rgb" |
| 3527 | 3674 | version = "0.8.53" |
@@ -3823,6 +3970,7 @@ version = "1.12.0"
| 3823 | 3970 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3824 | 3971 | checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" |
| 3825 | 3972 | dependencies = [ |
| 3973 | + "web-time", | |
| 3826 | 3974 | "zeroize", |
| 3827 | 3975 | ] |
| 3828 | 3976 |
@@ -4570,6 +4718,9 @@ name = "sync_wrapper"
| 4570 | 4718 | version = "1.0.2" |
| 4571 | 4719 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 4572 | 4720 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" |
| 4721 | +dependencies = [ | |
| 4722 | + "futures-core", | |
| 4723 | +] | |
| 4573 | 4724 | |
| 4574 | 4725 | [[package]] |
| 4575 | 4726 | name = "synstructure" |
@@ -4728,6 +4879,16 @@ dependencies = [
| 4728 | 4879 | "syn 2.0.106", |
| 4729 | 4880 | ] |
| 4730 | 4881 | |
| 4882 | +[[package]] | |
| 4883 | +name = "tokio-rustls" | |
| 4884 | +version = "0.26.4" | |
| 4885 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4886 | +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" | |
| 4887 | +dependencies = [ | |
| 4888 | + "rustls", | |
| 4889 | + "tokio", | |
| 4890 | +] | |
| 4891 | + | |
| 4731 | 4892 | [[package]] |
| 4732 | 4893 | name = "tokio-stream" |
| 4733 | 4894 | version = "0.1.17" |
@@ -4813,12 +4974,14 @@ dependencies = [
| 4813 | 4974 | "http-body-util", |
| 4814 | 4975 | "http-range-header", |
| 4815 | 4976 | "httpdate", |
| 4977 | + "iri-string", | |
| 4816 | 4978 | "mime", |
| 4817 | 4979 | "mime_guess", |
| 4818 | 4980 | "percent-encoding", |
| 4819 | 4981 | "pin-project-lite", |
| 4820 | 4982 | "tokio", |
| 4821 | 4983 | "tokio-util", |
| 4984 | + "tower", | |
| 4822 | 4985 | "tower-layer", |
| 4823 | 4986 | "tower-service", |
| 4824 | 4987 | "tracing", |
@@ -4898,6 +5061,12 @@ dependencies = [
| 4898 | 5061 | "tracing-log", |
| 4899 | 5062 | ] |
| 4900 | 5063 | |
| 5064 | +[[package]] | |
| 5065 | +name = "try-lock" | |
| 5066 | +version = "0.2.5" | |
| 5067 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5068 | +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" | |
| 5069 | + | |
| 4901 | 5070 | [[package]] |
| 4902 | 5071 | name = "typeid" |
| 4903 | 5072 | version = "1.0.3" |
@@ -5026,6 +5195,15 @@ version = "1.2.0"
| 5026 | 5195 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 5027 | 5196 | checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" |
| 5028 | 5197 | |
| 5198 | +[[package]] | |
| 5199 | +name = "want" | |
| 5200 | +version = "0.3.1" | |
| 5201 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5202 | +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" | |
| 5203 | +dependencies = [ | |
| 5204 | + "try-lock", | |
| 5205 | +] | |
| 5206 | + | |
| 5029 | 5207 | [[package]] |
| 5030 | 5208 | name = "wasi" |
| 5031 | 5209 | version = "0.11.1+wasi-snapshot-preview1" |
@@ -5138,6 +5316,16 @@ dependencies = [
| 5138 | 5316 | "wasm-bindgen", |
| 5139 | 5317 | ] |
| 5140 | 5318 | |
| 5319 | +[[package]] | |
| 5320 | +name = "web-time" | |
| 5321 | +version = "1.1.0" | |
| 5322 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5323 | +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" | |
| 5324 | +dependencies = [ | |
| 5325 | + "js-sys", | |
| 5326 | + "wasm-bindgen", | |
| 5327 | +] | |
| 5328 | + | |
| 5141 | 5329 | [[package]] |
| 5142 | 5330 | name = "webpki-roots" |
| 5143 | 5331 | version = "0.26.11" |
modified
Cargo.toml
+1
−0
@@ -57,6 +57,7 @@ ocrs = "0.10"
| 57 | 57 | rten = "0.21" |
| 58 | 58 | rten-imageproc = "0.21" |
| 59 | 59 | qrcode = { version = "0.14", default-features = false, features = ["svg"] } |
| 60 | +reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "rustls-tls"] } | |
| 60 | 61 | |
| 61 | 62 | [build-dependencies] |
| 62 | 63 | minijinja-embed = "2.12" |
modified
src/config.rs
+50
−0
@@ -13,6 +13,23 @@ pub struct Config {
| 13 | 13 | pub max_upload_bytes: usize, |
| 14 | 14 | pub auto_migrate: bool, |
| 15 | 15 | pub registration_enabled: bool, |
| 16 | + pub describe: DescribeConfig, | |
| 17 | +} | |
| 18 | + | |
| 19 | +#[derive(Debug, Clone)] | |
| 20 | +pub enum DescribeBackendKind { | |
| 21 | + Noop, | |
| 22 | + Rknn, | |
| 23 | +} | |
| 24 | + | |
| 25 | +#[derive(Debug, Clone)] | |
| 26 | +pub struct DescribeConfig { | |
| 27 | + pub backend: DescribeBackendKind, | |
| 28 | + pub endpoint: String, | |
| 29 | + pub model: String, | |
| 30 | + pub prompt: String, | |
| 31 | + pub timeout_secs: u64, | |
| 32 | + pub max_new_tokens: u32, | |
| 16 | 33 | } |
| 17 | 34 | |
| 18 | 35 | impl Config { |
@@ -48,10 +65,43 @@ impl Config {
| 48 | 65 | registration_enabled: env::var("REGISTRATION_ENABLED") |
| 49 | 66 | .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on")) |
| 50 | 67 | .unwrap_or(false), |
| 68 | + describe: DescribeConfig::from_env(), | |
| 51 | 69 | }) |
| 52 | 70 | } |
| 53 | 71 | } |
| 54 | 72 | |
| 73 | +impl DescribeConfig { | |
| 74 | + fn from_env() -> Self { | |
| 75 | + let backend = match env::var("DESCRIBE_BACKEND") | |
| 76 | + .unwrap_or_else(|_| "noop".to_string()) | |
| 77 | + .to_ascii_lowercase() | |
| 78 | + .as_str() | |
| 79 | + { | |
| 80 | + "rknn" | "rknn-llm" | "rknnllm" => DescribeBackendKind::Rknn, | |
| 81 | + _ => DescribeBackendKind::Noop, | |
| 82 | + }; | |
| 83 | + DescribeConfig { | |
| 84 | + backend, | |
| 85 | + endpoint: env::var("RKNN_LLM_ENDPOINT") | |
| 86 | + .unwrap_or_else(|_| "http://r5bp-0:8080/describe".to_string()), | |
| 87 | + model: env::var("RKNN_LLM_MODEL").unwrap_or_else(|_| "qwen2-vl-2b".to_string()), | |
| 88 | + prompt: env::var("RKNN_LLM_PROMPT").unwrap_or_else(|_| { | |
| 89 | + "Describe this image in one or two concise sentences. \ | |
| 90 | + List the salient objects, setting, and any visible text." | |
| 91 | + .to_string() | |
| 92 | + }), | |
| 93 | + timeout_secs: env::var("RKNN_LLM_TIMEOUT_SECS") | |
| 94 | + .ok() | |
| 95 | + .and_then(|v| v.parse().ok()) | |
| 96 | + .unwrap_or(120), | |
| 97 | + max_new_tokens: env::var("RKNN_LLM_MAX_NEW_TOKENS") | |
| 98 | + .ok() | |
| 99 | + .and_then(|v| v.parse().ok()) | |
| 100 | + .unwrap_or(256), | |
| 101 | + } | |
| 102 | + } | |
| 103 | +} | |
| 104 | + | |
| 55 | 105 | pub async fn create_db_connection(database_url: &str) -> Result<DatabaseConnection, sea_orm::DbErr> { |
| 56 | 106 | sea_orm::Database::connect(database_url).await |
| 57 | 107 | } |
modified
src/workers/describe.rs
+149
−13
@@ -1,34 +1,162 @@
| 1 | 1 | use async_trait::async_trait; |
| 2 | 2 | use futures_util::future::BoxFuture; |
| 3 | 3 | use sea_orm::*; |
| 4 | +use serde::{Deserialize, Serialize}; | |
| 5 | +use std::sync::Arc; | |
| 6 | +use std::time::Duration; | |
| 7 | +use tokio::sync::OnceCell; | |
| 4 | 8 | use tracing::{error, info, warn}; |
| 5 | 9 | |
| 10 | +use crate::config::{Config, DescribeBackendKind}; | |
| 6 | 11 | use crate::entities::{ai_descriptions, uploads}; |
| 7 | 12 | use crate::jobs; |
| 8 | 13 | use crate::queue::{publish_job, JobKind, JobMessage, JobOutcome}; |
| 9 | 14 | use crate::workers::WorkerContext; |
| 10 | 15 | |
| 11 | 16 | /// Pluggable backend for image description. Real implementations call out to |
| 12 | −/// a VLM (Anthropic, OpenAI, Ollama, etc); the noop variant just records a | |
| 13 | −/// placeholder so the rest of the pipeline can be exercised in dev. | |
| 17 | +/// a VLM (Anthropic, OpenAI, Ollama, rknn-llm); the noop variant just records | |
| 18 | +/// a placeholder so the rest of the pipeline can be exercised in dev. | |
| 14 | 19 | #[async_trait] |
| 15 | 20 | pub trait DescribeBackend: Send + Sync { |
| 16 | − async fn describe(&self, image_bytes: &[u8], content_type: Option<&str>) -> Result<String, String>; | |
| 17 | − fn model_name(&self) -> &'static str; | |
| 21 | + async fn describe( | |
| 22 | + &self, | |
| 23 | + image_bytes: &[u8], | |
| 24 | + content_type: Option<&str>, | |
| 25 | + filename: Option<&str>, | |
| 26 | + ) -> Result<String, String>; | |
| 27 | + fn model_name(&self) -> String; | |
| 18 | 28 | } |
| 19 | 29 | |
| 20 | 30 | pub struct NoopBackend; |
| 21 | 31 | |
| 22 | 32 | #[async_trait] |
| 23 | 33 | impl DescribeBackend for NoopBackend { |
| 24 | − async fn describe(&self, _image_bytes: &[u8], _content_type: Option<&str>) -> Result<String, String> { | |
| 34 | + async fn describe( | |
| 35 | + &self, | |
| 36 | + _image_bytes: &[u8], | |
| 37 | + _content_type: Option<&str>, | |
| 38 | + _filename: Option<&str>, | |
| 39 | + ) -> Result<String, String> { | |
| 25 | 40 | Ok(String::new()) |
| 26 | 41 | } |
| 27 | − fn model_name(&self) -> &'static str { | |
| 28 | − "noop" | |
| 42 | + fn model_name(&self) -> String { | |
| 43 | + "noop".to_string() | |
| 29 | 44 | } |
| 30 | 45 | } |
| 31 | 46 | |
| 47 | +/// HTTP client for the rknn-llm multimodal server (see | |
| 48 | +/// `tools/rknn-describe-server/` for the reference implementation). | |
| 49 | +/// | |
| 50 | +/// Wire protocol — POST `endpoint`, multipart/form-data: | |
| 51 | +/// - `image` binary file part | |
| 52 | +/// - `prompt` optional text part (overrides server default) | |
| 53 | +/// - `model` optional text part (selects loaded model on the server) | |
| 54 | +/// - `max_new_tokens` optional integer text part | |
| 55 | +/// | |
| 56 | +/// Response — JSON: `{ "description": "...", "model": "qwen2-vl-2b", "tokens": 123 }` | |
| 57 | +pub struct RknnBackend { | |
| 58 | + client: reqwest::Client, | |
| 59 | + endpoint: String, | |
| 60 | + model: String, | |
| 61 | + prompt: String, | |
| 62 | + max_new_tokens: u32, | |
| 63 | +} | |
| 64 | + | |
| 65 | +impl RknnBackend { | |
| 66 | + pub fn new(cfg: &crate::config::DescribeConfig) -> Result<Self, String> { | |
| 67 | + let client = reqwest::Client::builder() | |
| 68 | + .timeout(Duration::from_secs(cfg.timeout_secs)) | |
| 69 | + .build() | |
| 70 | + .map_err(|e| format!("build http client: {e}"))?; | |
| 71 | + Ok(RknnBackend { | |
| 72 | + client, | |
| 73 | + endpoint: cfg.endpoint.clone(), | |
| 74 | + model: cfg.model.clone(), | |
| 75 | + prompt: cfg.prompt.clone(), | |
| 76 | + max_new_tokens: cfg.max_new_tokens, | |
| 77 | + }) | |
| 78 | + } | |
| 79 | +} | |
| 80 | + | |
| 81 | +#[derive(Serialize, Deserialize)] | |
| 82 | +struct DescribeResponse { | |
| 83 | + description: String, | |
| 84 | + #[serde(default)] | |
| 85 | + model: Option<String>, | |
| 86 | +} | |
| 87 | + | |
| 88 | +#[async_trait] | |
| 89 | +impl DescribeBackend for RknnBackend { | |
| 90 | + async fn describe( | |
| 91 | + &self, | |
| 92 | + image_bytes: &[u8], | |
| 93 | + content_type: Option<&str>, | |
| 94 | + filename: Option<&str>, | |
| 95 | + ) -> Result<String, String> { | |
| 96 | + let mut part = reqwest::multipart::Part::bytes(image_bytes.to_vec()) | |
| 97 | + .file_name(filename.unwrap_or("upload.bin").to_string()); | |
| 98 | + if let Some(ct) = content_type { | |
| 99 | + part = part.mime_str(ct).map_err(|e| format!("invalid content-type: {e}"))?; | |
| 100 | + } | |
| 101 | + let form = reqwest::multipart::Form::new() | |
| 102 | + .part("image", part) | |
| 103 | + .text("prompt", self.prompt.clone()) | |
| 104 | + .text("model", self.model.clone()) | |
| 105 | + .text("max_new_tokens", self.max_new_tokens.to_string()); | |
| 106 | + | |
| 107 | + let resp = self | |
| 108 | + .client | |
| 109 | + .post(&self.endpoint) | |
| 110 | + .multipart(form) | |
| 111 | + .send() | |
| 112 | + .await | |
| 113 | + .map_err(|e| format!("rknn-llm POST {}: {e}", self.endpoint))?; | |
| 114 | + let status = resp.status(); | |
| 115 | + if !status.is_success() { | |
| 116 | + let body = resp.text().await.unwrap_or_default(); | |
| 117 | + return Err(format!("rknn-llm http {status}: {body}")); | |
| 118 | + } | |
| 119 | + let parsed: DescribeResponse = resp | |
| 120 | + .json() | |
| 121 | + .await | |
| 122 | + .map_err(|e| format!("decode rknn-llm response: {e}"))?; | |
| 123 | + Ok(parsed.description) | |
| 124 | + } | |
| 125 | + fn model_name(&self) -> String { | |
| 126 | + format!("rknn-llm:{}", self.model) | |
| 127 | + } | |
| 128 | +} | |
| 129 | + | |
| 130 | +static BACKEND: OnceCell<Arc<dyn DescribeBackend>> = OnceCell::const_new(); | |
| 131 | + | |
| 132 | +async fn backend_for(cfg: &Config) -> Arc<dyn DescribeBackend> { | |
| 133 | + BACKEND | |
| 134 | + .get_or_init(|| async { | |
| 135 | + match cfg.describe.backend { | |
| 136 | + DescribeBackendKind::Rknn => match RknnBackend::new(&cfg.describe) { | |
| 137 | + Ok(b) => { | |
| 138 | + info!( | |
| 139 | + endpoint = %cfg.describe.endpoint, | |
| 140 | + model = %cfg.describe.model, | |
| 141 | + "describe backend: rknn-llm" | |
| 142 | + ); | |
| 143 | + Arc::new(b) as Arc<dyn DescribeBackend> | |
| 144 | + } | |
| 145 | + Err(e) => { | |
| 146 | + error!(error = %e, "rknn backend init failed; falling back to noop"); | |
| 147 | + Arc::new(NoopBackend) as Arc<dyn DescribeBackend> | |
| 148 | + } | |
| 149 | + }, | |
| 150 | + DescribeBackendKind::Noop => { | |
| 151 | + info!("describe backend: noop"); | |
| 152 | + Arc::new(NoopBackend) as Arc<dyn DescribeBackend> | |
| 153 | + } | |
| 154 | + } | |
| 155 | + }) | |
| 156 | + .await | |
| 157 | + .clone() | |
| 158 | +} | |
| 159 | + | |
| 32 | 160 | pub fn run(ctx: WorkerContext, msg: JobMessage) -> BoxFuture<'static, JobOutcome> { |
| 33 | 161 | Box::pin(async move { |
| 34 | 162 | if matches!(jobs::is_complete(ctx.db.as_ref(), msg.upload_id, JobKind::Describe).await, Ok(true)) { |
@@ -37,8 +165,7 @@ pub fn run(ctx: WorkerContext, msg: JobMessage) -> BoxFuture<'static, JobOutcome
| 37 | 165 | } |
| 38 | 166 | let _ = jobs::mark_processing(ctx.db.as_ref(), msg.upload_id, JobKind::Describe).await; |
| 39 | 167 | |
| 40 | − // For now use the noop backend; swap in a real VLM client once chosen. | |
| 41 | − let backend: Box<dyn DescribeBackend> = Box::new(NoopBackend); | |
| 168 | + let backend = backend_for(ctx.config.as_ref()).await; | |
| 42 | 169 | |
| 43 | 170 | let upload = match uploads::Entity::find_by_id(msg.upload_id) |
| 44 | 171 | .one(ctx.db.as_ref()) |
@@ -65,12 +192,21 @@ pub fn run(ctx: WorkerContext, msg: JobMessage) -> BoxFuture<'static, JobOutcome
| 65 | 192 | } |
| 66 | 193 | }; |
| 67 | 194 | |
| 68 | − let description = match backend.describe(&bytes, upload.content_type.as_deref()).await { | |
| 195 | + let filename = std::path::Path::new(&upload.item_path) | |
| 196 | + .file_name() | |
| 197 | + .and_then(|s| s.to_str()); | |
| 198 | + | |
| 199 | + let description = match backend | |
| 200 | + .describe(&bytes, upload.content_type.as_deref(), filename) | |
| 201 | + .await | |
| 202 | + { | |
| 69 | 203 | Ok(d) => d, |
| 70 | 204 | Err(e) => { |
| 71 | 205 | warn!(error = %e, "describe backend failed"); |
| 206 | + // Network/transport errors are retryable; persistent ones get | |
| 207 | + // surfaced via the dead-letter queue when retries exhaust. | |
| 72 | 208 | let _ = jobs::mark_failed(ctx.db.as_ref(), upload.id, JobKind::Describe, &e).await; |
| 73 | − return JobOutcome::Reject; | |
| 209 | + return JobOutcome::Retry; | |
| 74 | 210 | } |
| 75 | 211 | }; |
| 76 | 212 |
@@ -78,7 +214,7 @@ pub fn run(ctx: WorkerContext, msg: JobMessage) -> BoxFuture<'static, JobOutcome
| 78 | 214 | let active = ai_descriptions::ActiveModel { |
| 79 | 215 | upload_id: Set(upload.id), |
| 80 | 216 | description: Set(description.clone()), |
| 81 | − model_used: Set(Some(backend.model_name().to_string())), | |
| 217 | + model_used: Set(Some(backend.model_name())), | |
| 82 | 218 | confidence: Set(None), |
| 83 | 219 | ..Default::default() |
| 84 | 220 | }; |
@@ -96,7 +232,7 @@ pub fn run(ctx: WorkerContext, msg: JobMessage) -> BoxFuture<'static, JobOutcome
| 96 | 232 | } |
| 97 | 233 | info!(upload_id = upload.id, "description recorded"); |
| 98 | 234 | } else { |
| 99 | − info!(upload_id = upload.id, "noop describe; marking complete with empty result"); | |
| 235 | + info!(upload_id = upload.id, "empty describe result; marking complete"); | |
| 100 | 236 | let _ = jobs::mark_complete(ctx.db.as_ref(), upload.id, JobKind::Describe).await; |
| 101 | 237 | } |
| 102 | 238 | JobOutcome::Ack |
added
tools/rknn-describe-server/README.md
+122
−0
@@ -0,0 +1,122 @@
| 1 | +# rknn-describe-server | |
| 2 | + | |
| 3 | +A small HTTP server that runs **on the Rockchip board** (RK3588 / RK3576 — e.g. | |
| 4 | +`r5bp-0`) and exposes the [airockchip/rknn-llm](https://github.com/airockchip/rknn-llm) | |
| 5 | +multimodal demo over a simple multipart endpoint that the DMS describe worker | |
| 6 | +can call. | |
| 7 | + | |
| 8 | +The DMS Rust worker stays on x86; this server is the only thing that needs to | |
| 9 | +live on the NPU host. | |
| 10 | + | |
| 11 | +## Wire protocol | |
| 12 | + | |
| 13 | +`POST /describe` — multipart/form-data: | |
| 14 | + | |
| 15 | +| field | required | notes | | |
| 16 | +| ---------------- | -------- | --------------------------------------------------------- | | |
| 17 | +| `image` | yes | binary image (jpg/png/webp/...) | | |
| 18 | +| `prompt` | no | overrides default prompt | | |
| 19 | +| `model` | no | name registered in `models.yaml` (default: first entry) | | |
| 20 | +| `max_new_tokens` | no | integer, default 256 | | |
| 21 | + | |
| 22 | +Response: | |
| 23 | + | |
| 24 | +```json | |
| 25 | +{ "description": "A red barn at dusk with a tractor in front.", | |
| 26 | + "model": "qwen2-vl-2b", | |
| 27 | + "tokens": 27 } | |
| 28 | +``` | |
| 29 | + | |
| 30 | +`GET /healthz` — `200 ok` once the model is loaded. | |
| 31 | + | |
| 32 | +## Approach | |
| 33 | + | |
| 34 | +The reference implementation here just shells out to the upstream | |
| 35 | +`multimodal_demo` binary per request. That keeps the integration minimal but | |
| 36 | +**reloads the model every call** — fine for a smoke test, far too slow for | |
| 37 | +production. | |
| 38 | + | |
| 39 | +For sustained throughput, replace `subprocess` with a long-lived ctypes | |
| 40 | +wrapper around `librkllmrt.so` (see `rkllm.h` in the upstream repo) that | |
| 41 | +keeps the language model warm across requests. The request/response shape | |
| 42 | +above does not need to change. | |
| 43 | + | |
| 44 | +## Setup on r5bp-0 (RK3588, librknnrt 2.3.0 already shipped by Armbian) | |
| 45 | + | |
| 46 | +The bring-up below is what we ran on `r5bp-0` and should reproduce on any | |
| 47 | +RK3588 board with the same OS image. | |
| 48 | + | |
| 49 | +```bash | |
| 50 | +sudo apt install -y git cmake g++ python3-venv python3-pip | |
| 51 | + | |
| 52 | +# 1. clone the upstream toolkit and build the multimodal demo (the cmake | |
| 53 | +# build-linux.sh assumes a cross-compiler — on-device just call cmake | |
| 54 | +# directly with the system toolchain). | |
| 55 | +mkdir -p ~/dms-vlm && cd ~/dms-vlm | |
| 56 | +git clone --depth 1 https://github.com/airockchip/rknn-llm.git | |
| 57 | +cd rknn-llm/examples/multimodal_model_demo/deploy | |
| 58 | +mkdir build && cd build | |
| 59 | +cmake .. -DCMAKE_BUILD_TYPE=Release \ | |
| 60 | + -DCMAKE_SYSTEM_NAME=Linux \ | |
| 61 | + -DCMAKE_SYSTEM_PROCESSOR=aarch64 | |
| 62 | +make -j8 install | |
| 63 | +# ⇒ ./install/demo_Linux_aarch64/{demo,imgenc,lib/} | |
| 64 | + | |
| 65 | +# 2. fetch a converted VLM. The Qwen2-VL-2B build below totals ~3.3 GiB. | |
| 66 | +mkdir -p ~/dms-vlm/models/qwen2-vl-2b && cd ~/dms-vlm/models/qwen2-vl-2b | |
| 67 | +wget -O Qwen2-VL-2B-Instruct.rkllm \ | |
| 68 | + https://huggingface.co/thanhtantran/Qwen2-VL-2B-RKLLM/resolve/main/Qwen2-VL-2B-Instruct.rkllm | |
| 69 | +wget -O Qwen2-VL-2B-Instruct.rknn \ | |
| 70 | + https://huggingface.co/thanhtantran/Qwen2-VL-2B-RKLLM/resolve/main/Qwen2-VL-2B-Instruct.rknn | |
| 71 | + | |
| 72 | +# 3. drop server.py + models.yaml into ~/dms-vlm, create a venv, install deps | |
| 73 | +cd ~/dms-vlm | |
| 74 | +python3 -m venv venv | |
| 75 | +./venv/bin/pip install fastapi "uvicorn[standard]" python-multipart pyyaml | |
| 76 | + | |
| 77 | +# 4. install the user-level systemd unit and enable linger so it keeps | |
| 78 | +# running across reboots without an active login session. | |
| 79 | +mkdir -p ~/.config/systemd/user | |
| 80 | +install -m 644 dms-vlm.service ~/.config/systemd/user/ | |
| 81 | +systemctl --user daemon-reload | |
| 82 | +systemctl --user enable --now dms-vlm.service | |
| 83 | +sudo loginctl enable-linger "$USER" | |
| 84 | +``` | |
| 85 | + | |
| 86 | +Smoke test from another host: | |
| 87 | + | |
| 88 | +```bash | |
| 89 | +curl -s http://r5bp-0:8080/healthz | |
| 90 | +curl -F image=@/path/to/sample.jpg http://r5bp-0:8080/describe | |
| 91 | +``` | |
| 92 | + | |
| 93 | +First request takes ~10–15 s because the demo binary reloads the model | |
| 94 | +every call. That's *the* thing to fix when going to production — replace | |
| 95 | +`subprocess` with a long-lived ctypes wrapper around `librkllmrt.so` so | |
| 96 | +the LLM stays warm. The wire contract above does not change. | |
| 97 | + | |
| 98 | +`models.yaml` example: | |
| 99 | + | |
| 100 | +```yaml | |
| 101 | +qwen2-vl-2b: | |
| 102 | + vision: /opt/rknn-llm/models/qwen2-vl-2b/vision.rknn | |
| 103 | + llm: /opt/rknn-llm/models/qwen2-vl-2b/llm.rkllm | |
| 104 | + max_context_len: 2048 | |
| 105 | + rknn_core_num: 3 | |
| 106 | + img_start: "<|vision_start|>" | |
| 107 | + img_end: "<|vision_end|>" | |
| 108 | + img_content: "<|image_pad|>" | |
| 109 | +minicpm-v-2_6: | |
| 110 | + vision: /opt/rknn-llm/models/minicpm-v-2_6/vision.rknn | |
| 111 | + llm: /opt/rknn-llm/models/minicpm-v-2_6/llm.rkllm | |
| 112 | + max_context_len: 2048 | |
| 113 | + rknn_core_num: 3 | |
| 114 | +internvl2-1b: | |
| 115 | + vision: /opt/rknn-llm/models/internvl2-1b/vision.rknn | |
| 116 | + llm: /opt/rknn-llm/models/internvl2-1b/llm.rkllm | |
| 117 | + max_context_len: 2048 | |
| 118 | + rknn_core_num: 3 | |
| 119 | +``` | |
| 120 | + | |
| 121 | +The DMS worker then talks to `http://r5bp-0:8080/describe` — see | |
| 122 | +`workers.yml` for the env-var wiring. |
added
tools/rknn-describe-server/dms-vlm.service
+19
−0
@@ -0,0 +1,19 @@
| 1 | +[Unit] | |
| 2 | +Description=DMS rknn-llm describe server | |
| 3 | +After=network-online.target | |
| 4 | +Wants=network-online.target | |
| 5 | + | |
| 6 | +[Service] | |
| 7 | +Type=simple | |
| 8 | +WorkingDirectory=%h/dms-vlm | |
| 9 | +ExecStart=%h/dms-vlm/venv/bin/python server.py \ | |
| 10 | + --models-config %h/dms-vlm/models.yaml \ | |
| 11 | + --demo-bin %h/dms-vlm/rknn-llm/examples/multimodal_model_demo/deploy/install/demo_Linux_aarch64/demo \ | |
| 12 | + --bind 0.0.0.0:8080 | |
| 13 | +Restart=on-failure | |
| 14 | +RestartSec=5 | |
| 15 | +StandardOutput=journal | |
| 16 | +StandardError=journal | |
| 17 | + | |
| 18 | +[Install] | |
| 19 | +WantedBy=default.target |
added
tools/rknn-describe-server/server.py
+170
−0
@@ -0,0 +1,170 @@
| 1 | +#!/usr/bin/env python3 | |
| 2 | +"""HTTP wrapper around the rknn-llm multimodal_demo binary. | |
| 3 | + | |
| 4 | +Wire contract documented in README.md. Each /describe request: | |
| 5 | + 1. saves the uploaded image to a tmp file | |
| 6 | + 2. spawns ./demo with that image; demo loads vision encoder + LLM, encodes | |
| 7 | + the image once, then enters an interactive REPL on stdin | |
| 8 | + 3. we feed it `<image><prompt>\\n` and `exit\\n`, capture stdout, and | |
| 9 | + extract the text after the "robot: " marker | |
| 10 | + | |
| 11 | +This reloads the model per request -- fine for first-cut validation, slow | |
| 12 | +for production. Replace with a long-lived ctypes wrapper around | |
| 13 | +librkllmrt.so once the integration is proven end-to-end. | |
| 14 | +""" | |
| 15 | +from __future__ import annotations | |
| 16 | + | |
| 17 | +import argparse | |
| 18 | +import os | |
| 19 | +import re | |
| 20 | +import shutil | |
| 21 | +import subprocess | |
| 22 | +import tempfile | |
| 23 | +import time | |
| 24 | +from pathlib import Path | |
| 25 | + | |
| 26 | +import yaml | |
| 27 | +from fastapi import FastAPI, File, Form, HTTPException, UploadFile | |
| 28 | +from fastapi.responses import JSONResponse | |
| 29 | +import uvicorn | |
| 30 | + | |
| 31 | + | |
| 32 | +def load_models(path: str) -> dict: | |
| 33 | + with open(path) as f: | |
| 34 | + cfg = yaml.safe_load(f) or {} | |
| 35 | + if not cfg: | |
| 36 | + raise SystemExit(f"no models defined in {path}") | |
| 37 | + for name, m in cfg.items(): | |
| 38 | + for key in ("vision", "llm"): | |
| 39 | + if not Path(m[key]).exists(): | |
| 40 | + raise SystemExit(f"model {name}: {key} missing: {m[key]}") | |
| 41 | + return cfg | |
| 42 | + | |
| 43 | + | |
| 44 | +# multimodal_demo prints `robot: <text>\n` after each prompt; we want the text | |
| 45 | +# from the *last* `robot:` (after our own prompt, not the startup banner). | |
| 46 | +ROBOT_RE = re.compile(r"robot:\s*(.*?)(?=\nuser:|\Z)", re.DOTALL) | |
| 47 | + | |
| 48 | + | |
| 49 | +def run_demo(demo_bin: str, demo_dir: str, image_path: str, model_cfg: dict, | |
| 50 | + prompt: str, max_new_tokens: int, max_context_len: int, | |
| 51 | + timeout: int) -> tuple[str, str]: | |
| 52 | + abs_demo = os.path.abspath(demo_bin) | |
| 53 | + cmd = [ | |
| 54 | + abs_demo, | |
| 55 | + image_path, | |
| 56 | + model_cfg["vision"], | |
| 57 | + model_cfg["llm"], | |
| 58 | + str(max_new_tokens), | |
| 59 | + str(max_context_len), | |
| 60 | + str(model_cfg.get("rknn_core_num", 3)), | |
| 61 | + model_cfg.get("img_start", "<|vision_start|>"), | |
| 62 | + model_cfg.get("img_end", "<|vision_end|>"), | |
| 63 | + model_cfg.get("img_content", "<|image_pad|>"), | |
| 64 | + ] | |
| 65 | + env = dict(os.environ) | |
| 66 | + # cwd is changed before exec, so use absolute lib path or the dynamic | |
| 67 | + # linker can't find librkllmrt.so / librknnrt.so. | |
| 68 | + lib_dir = str(Path(abs_demo).parent / "lib") | |
| 69 | + env["LD_LIBRARY_PATH"] = lib_dir + ":" + env.get("LD_LIBRARY_PATH", "") | |
| 70 | + | |
| 71 | + # Demo's REPL: send `<image>{prompt}\n`, then `exit\n` to break the loop. | |
| 72 | + # The pre-canned numeric shortcuts (`0`, `1`) are ignored because our | |
| 73 | + # input is plain text starting with `<image>`. | |
| 74 | + stdin_payload = f"<image>{prompt}\nexit\n" | |
| 75 | + proc = subprocess.run( | |
| 76 | + cmd, | |
| 77 | + input=stdin_payload, | |
| 78 | + capture_output=True, | |
| 79 | + text=True, | |
| 80 | + cwd=demo_dir, | |
| 81 | + env=env, | |
| 82 | + timeout=timeout, | |
| 83 | + ) | |
| 84 | + return proc.stdout, proc.stderr | |
| 85 | + | |
| 86 | + | |
| 87 | +def extract_robot_reply(stdout: str) -> str: | |
| 88 | + # The interactive prompt prints "user: " before each line, then "robot: " | |
| 89 | + # followed by streamed tokens, then a newline at finish. Take the LAST | |
| 90 | + # robot block to ignore any startup chatter. | |
| 91 | + matches = ROBOT_RE.findall(stdout) | |
| 92 | + if not matches: | |
| 93 | + return "" | |
| 94 | + return matches[-1].strip() | |
| 95 | + | |
| 96 | + | |
| 97 | +def make_app(models: dict, demo_bin: str, default_prompt: str) -> FastAPI: | |
| 98 | + app = FastAPI() | |
| 99 | + default_model = next(iter(models)) | |
| 100 | + demo_dir = str(Path(demo_bin).parent) | |
| 101 | + | |
| 102 | + @app.get("/healthz") | |
| 103 | + def healthz(): | |
| 104 | + return {"ok": True, "models": list(models.keys()), "default": default_model} | |
| 105 | + | |
| 106 | + @app.post("/describe") | |
| 107 | + async def describe( | |
| 108 | + image: UploadFile = File(...), | |
| 109 | + prompt: str = Form(default_prompt), | |
| 110 | + model: str = Form(default_model), | |
| 111 | + max_new_tokens: int = Form(256), | |
| 112 | + ): | |
| 113 | + if model not in models: | |
| 114 | + raise HTTPException(404, f"unknown model {model!r}; have {list(models)}") | |
| 115 | + m = models[model] | |
| 116 | + max_context_len = int(m.get("max_context_len", 4096)) | |
| 117 | + | |
| 118 | + with tempfile.TemporaryDirectory() as td: | |
| 119 | + ext = Path(image.filename or "in.bin").suffix or ".bin" | |
| 120 | + img_path = Path(td) / f"input{ext}" | |
| 121 | + with img_path.open("wb") as f: | |
| 122 | + shutil.copyfileobj(image.file, f) | |
| 123 | + | |
| 124 | + t0 = time.monotonic() | |
| 125 | + try: | |
| 126 | + stdout, stderr = run_demo( | |
| 127 | + demo_bin, demo_dir, str(img_path), m, | |
| 128 | + prompt, max_new_tokens, max_context_len, | |
| 129 | + timeout=int(os.environ.get("RKNN_DEMO_TIMEOUT", "300")), | |
| 130 | + ) | |
| 131 | + except subprocess.TimeoutExpired: | |
| 132 | + return JSONResponse({"error": "demo timeout"}, status_code=504) | |
| 133 | + elapsed = time.monotonic() - t0 | |
| 134 | + | |
| 135 | + text = extract_robot_reply(stdout) | |
| 136 | + if not text: | |
| 137 | + return JSONResponse( | |
| 138 | + {"error": "no reply extracted", | |
| 139 | + "stdout_tail": stdout[-2000:], | |
| 140 | + "stderr_tail": stderr[-2000:]}, | |
| 141 | + status_code=500, | |
| 142 | + ) | |
| 143 | + return { | |
| 144 | + "description": text, | |
| 145 | + "model": model, | |
| 146 | + "elapsed_seconds": round(elapsed, 2), | |
| 147 | + } | |
| 148 | + | |
| 149 | + return app | |
| 150 | + | |
| 151 | + | |
| 152 | +def main(): | |
| 153 | + ap = argparse.ArgumentParser() | |
| 154 | + ap.add_argument("--models-config", default="models.yaml") | |
| 155 | + ap.add_argument("--demo-bin", required=True, | |
| 156 | + help="path to the built rknn-llm multimodal demo binary") | |
| 157 | + ap.add_argument("--bind", default="0.0.0.0:8080") | |
| 158 | + ap.add_argument("--default-prompt", default=( | |
| 159 | + "Describe this image in one or two concise sentences. " | |
| 160 | + "List the salient objects, setting, and any visible text.")) | |
| 161 | + args = ap.parse_args() | |
| 162 | + | |
| 163 | + models = load_models(args.models_config) | |
| 164 | + app = make_app(models, args.demo_bin, args.default_prompt) | |
| 165 | + host, port = args.bind.split(":") | |
| 166 | + uvicorn.run(app, host=host, port=int(port)) | |
| 167 | + | |
| 168 | + | |
| 169 | +if __name__ == "__main__": | |
| 170 | + main() |
Parents: 7786f28