Compare commits
20 Commits
383201cacd
...
v0.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 37d2b66c56 | |||
| 95b44f1e55 | |||
| 2393d81a3f | |||
| 82955504f3 | |||
| 80399b1c12 | |||
| ceb33877ff | |||
| dacfb7e083 | |||
| fb60858cec | |||
| f1d7077e40 | |||
| 4f402c873b | |||
| a89d94bc67 | |||
| 17cab667f9 | |||
| f8925ab994 | |||
| dac2b234d0 | |||
| 67c8c60ceb | |||
| c5ba252d89 | |||
| e4d3365ada | |||
| de80b96c15 | |||
| f5436c7b63 | |||
| 845704724f |
@@ -1,40 +1,5 @@
|
|||||||
# Database Configuration
|
DB_MAX_CONNECTIONS=50
|
||||||
DATABASE_URL=postgres://accusys@localhost:5432/momentry
|
DB_ACQUIRE_TIMEOUT=30
|
||||||
|
QDRANT_URL=http://127.0.0.1:6333
|
||||||
# Redis
|
|
||||||
# Format: redis://[username][:password]@host:port
|
|
||||||
# Users: default (with password), accusys (custom user with password)
|
|
||||||
REDIS_URL=redis://accusys:accusys@localhost:6379
|
|
||||||
|
|
||||||
# MongoDB
|
|
||||||
MONGODB_URL=mongodb://accusys:Test3200Test3200@localhost:27017/admin
|
|
||||||
MONGODB_DATABASE=momentry
|
|
||||||
|
|
||||||
# Qdrant Vector Database
|
|
||||||
QDRANT_URL=http://localhost:6333
|
|
||||||
QDRANT_API_KEY=Test3200Test3200Test3200
|
QDRANT_API_KEY=Test3200Test3200Test3200
|
||||||
QDRANT_COLLECTION=chunks_v3
|
QDRANT_COLLECTION=momentry_rule1
|
||||||
|
|
||||||
# Gitea
|
|
||||||
GITEA_URL=http://localhost:3000
|
|
||||||
|
|
||||||
# API Server (Production)
|
|
||||||
MOMENTRY_SERVER_PORT=3002
|
|
||||||
MOMENTRY_REDIS_PREFIX=momentry:
|
|
||||||
API_HOST=127.0.0.1
|
|
||||||
API_PORT=3002
|
|
||||||
|
|
||||||
# Worker Configuration (Production)
|
|
||||||
MOMENTRY_WORKER_ENABLED=true
|
|
||||||
MOMENTRY_MAX_CONCURRENT=2
|
|
||||||
MOMENTRY_POLL_INTERVAL=5
|
|
||||||
|
|
||||||
# Watch Directories (comma separated)
|
|
||||||
WATCH_DIRECTORIES=~/Videos,~/momentry_core_project/test_video
|
|
||||||
|
|
||||||
# Ollama (for Mistral 7B LLM)
|
|
||||||
OLLAMA_HOST=http://localhost:11434
|
|
||||||
|
|
||||||
# Model Paths
|
|
||||||
# EMBEDDING_MODEL_PATH=./models/comic-embed-text
|
|
||||||
# LLM_MODEL_PATH=./models/mistral-7b
|
|
||||||
+3
-3
@@ -18,7 +18,7 @@ MOMENTRY_WORKER_BATCH_SIZE=5
|
|||||||
DATABASE_URL=postgres://accusys@localhost:5432/momentry
|
DATABASE_URL=postgres://accusys@localhost:5432/momentry
|
||||||
|
|
||||||
# MongoDB
|
# MongoDB
|
||||||
MONGODB_URL=mongodb://accusys:Test3200Test3200@localhost:27017/admin
|
MONGODB_URL=mongodb://localhost:27017
|
||||||
MONGODB_DATABASE=momentry
|
MONGODB_DATABASE=momentry
|
||||||
|
|
||||||
# Redis
|
# Redis
|
||||||
@@ -28,7 +28,7 @@ REDIS_PASSWORD=accusys
|
|||||||
# Qdrant Vector Database (same as production)
|
# Qdrant Vector Database (same as production)
|
||||||
QDRANT_URL=http://localhost:6333
|
QDRANT_URL=http://localhost:6333
|
||||||
QDRANT_API_KEY=Test3200Test3200Test3200
|
QDRANT_API_KEY=Test3200Test3200Test3200
|
||||||
QDRANT_COLLECTION=chunks_v3
|
QDRANT_COLLECTION=momentry_rule1
|
||||||
|
|
||||||
# Paths
|
# Paths
|
||||||
MOMENTRY_OUTPUT_DIR=/Users/accusys/momentry/output_dev
|
MOMENTRY_OUTPUT_DIR=/Users/accusys/momentry/output_dev
|
||||||
@@ -51,7 +51,7 @@ MOMENTRY_CUT_TIMEOUT=3600
|
|||||||
MOMENTRY_DEFAULT_TIMEOUT=7200
|
MOMENTRY_DEFAULT_TIMEOUT=7200
|
||||||
|
|
||||||
# Cache Settings
|
# Cache Settings
|
||||||
MONGODB_CACHE_ENABLED=true
|
MONGODB_CACHE_ENABLED=false
|
||||||
MONGODB_CACHE_TTL_VIDEOS=300
|
MONGODB_CACHE_TTL_VIDEOS=300
|
||||||
MONGODB_CACHE_TTL_SEARCH=300
|
MONGODB_CACHE_TTL_SEARCH=300
|
||||||
MONGODB_CACHE_TTL_HYBRID_SEARCH=600
|
MONGODB_CACHE_TTL_HYBRID_SEARCH=600
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ MONGODB_DATABASE=momentry
|
|||||||
# ===========================================
|
# ===========================================
|
||||||
QDRANT_URL=http://localhost:6333
|
QDRANT_URL=http://localhost:6333
|
||||||
QDRANT_API_KEY=your_qdrant_api_key
|
QDRANT_API_KEY=your_qdrant_api_key
|
||||||
QDRANT_COLLECTION=chunks_v3
|
QDRANT_COLLECTION=momentry_rule1
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# API Server Configuration
|
# API Server Configuration
|
||||||
|
|||||||
@@ -38,3 +38,6 @@ id_*
|
|||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
|
||||||
|
# Documentation backups
|
||||||
|
docs_v1.0/
|
||||||
|
|||||||
Generated
+405
-4
@@ -8,6 +8,12 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler32"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aead"
|
name = "aead"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -279,6 +285,7 @@ dependencies = [
|
|||||||
"matchit",
|
"matchit",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mime",
|
"mime",
|
||||||
|
"multer",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
@@ -348,6 +355,26 @@ version = "1.8.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bincode"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
|
||||||
|
dependencies = [
|
||||||
|
"bincode_derive",
|
||||||
|
"serde",
|
||||||
|
"unty",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bincode_derive"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
|
||||||
|
dependencies = [
|
||||||
|
"virtue",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@@ -441,6 +468,25 @@ dependencies = [
|
|||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cbindgen"
|
||||||
|
version = "0.29.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "befbfd072a8e81c02f8c507aefce431fe5e7d051f83d48a23ffc9b9fe5a11799"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"heck 0.5.0",
|
||||||
|
"indexmap 2.13.0",
|
||||||
|
"log",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"syn 2.0.117",
|
||||||
|
"tempfile",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.57"
|
version = "1.2.57"
|
||||||
@@ -448,9 +494,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cedarwood"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d910bedd62c24733263d0bed247460853c9d22e8956bd4cd964302095e04e90"
|
||||||
|
dependencies = [
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -608,6 +665,15 @@ version = "0.8.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core2"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -823,6 +889,12 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dary_heap"
|
||||||
|
version = "0.3.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-encoding"
|
name = "data-encoding"
|
||||||
version = "2.10.0"
|
version = "2.10.0"
|
||||||
@@ -1057,6 +1129,37 @@ version = "2.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ferrous-opencc"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "800f9a39ad26b200b08673c059c2dbf78d1f63901a873e9ac425ba471e690410"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bincode",
|
||||||
|
"cbindgen",
|
||||||
|
"ferrous-opencc-compiler",
|
||||||
|
"fst",
|
||||||
|
"phf",
|
||||||
|
"phf_codegen",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"zstd",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ferrous-opencc-compiler"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ed87f42a19a234e791d6e769928819382c3d210da2c9049f832a1ae772d7082"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bincode",
|
||||||
|
"fst",
|
||||||
|
"zstd",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filetime"
|
name = "filetime"
|
||||||
version = "0.2.27"
|
version = "0.2.27"
|
||||||
@@ -1107,6 +1210,12 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -1140,6 +1249,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fst"
|
||||||
|
version = "0.4.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "funty"
|
name = "funty"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -1338,7 +1453,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"foldhash",
|
"foldhash 0.1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1346,6 +1461,11 @@ name = "hashbrown"
|
|||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
|
"foldhash 0.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashlink"
|
name = "hashlink"
|
||||||
@@ -1698,6 +1818,39 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include-flate"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a05fb00d9abc625268e0573a519506b264a7d6965de09bac13201bfb44e723d"
|
||||||
|
dependencies = [
|
||||||
|
"include-flate-codegen",
|
||||||
|
"include-flate-compress",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include-flate-codegen"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92c3c319a7527668538a8530c541e74e881e94c4f41e1425622d0a41c16468af"
|
||||||
|
dependencies = [
|
||||||
|
"include-flate-compress",
|
||||||
|
"proc-macro-error2",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include-flate-compress"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed0bd9ea81b94169d61c5a397e9faef02153d3711fc62d3270bcde3ac85380d9"
|
||||||
|
dependencies = [
|
||||||
|
"libflate",
|
||||||
|
"zstd",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "1.9.3"
|
||||||
@@ -1829,6 +1982,29 @@ version = "1.0.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jieba-macros"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "348294e44ee7e3c42685da656490f8febc7359632544019621588902216da95c"
|
||||||
|
dependencies = [
|
||||||
|
"phf_codegen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jieba-rs"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "766bd7012aa5ba49411ebdf4e93bddd59b182d2918e085d58dec5bb9b54b7105"
|
||||||
|
dependencies = [
|
||||||
|
"cedarwood",
|
||||||
|
"include-flate",
|
||||||
|
"jieba-macros",
|
||||||
|
"phf",
|
||||||
|
"regex",
|
||||||
|
"rustc-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff"
|
name = "jiff"
|
||||||
version = "0.2.23"
|
version = "0.2.23"
|
||||||
@@ -1853,6 +2029,16 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.91"
|
version = "0.3.91"
|
||||||
@@ -1904,6 +2090,30 @@ version = "0.2.183"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libflate"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3248b8d211bd23a104a42d81b4fa8bb8ac4a3b75e7a43d85d2c9ccb6179cd74"
|
||||||
|
dependencies = [
|
||||||
|
"adler32",
|
||||||
|
"core2",
|
||||||
|
"crc32fast",
|
||||||
|
"dary_heap",
|
||||||
|
"libflate_lz77",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libflate_lz77"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a599cb10a9cd92b1300debcef28da8f70b935ec937f44fcd1b70a7c986a11c5c"
|
||||||
|
dependencies = [
|
||||||
|
"core2",
|
||||||
|
"hashbrown 0.16.1",
|
||||||
|
"rle-decode-fast",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libm"
|
name = "libm"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@@ -2116,8 +2326,10 @@ dependencies = [
|
|||||||
"crossterm",
|
"crossterm",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"ferrous-opencc",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
|
"jieba-rs",
|
||||||
"libc",
|
"libc",
|
||||||
"md5",
|
"md5",
|
||||||
"moka",
|
"moka",
|
||||||
@@ -2190,6 +2402,23 @@ dependencies = [
|
|||||||
"webpki-roots 0.25.4",
|
"webpki-roots 0.25.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "multer"
|
||||||
|
version = "3.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"encoding_rs",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"spin",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.18"
|
version = "0.2.18"
|
||||||
@@ -2440,6 +2669,59 @@ version = "2.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||||
|
dependencies = [
|
||||||
|
"phf_macros",
|
||||||
|
"phf_shared",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_codegen"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator",
|
||||||
|
"phf_shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"phf_shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_macros"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator",
|
||||||
|
"phf_shared",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.11"
|
version = "1.1.11"
|
||||||
@@ -2599,6 +2881,28 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error-attr2"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error-attr2",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@@ -2966,6 +3270,12 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rle-decode-fast"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.10"
|
version = "0.9.10"
|
||||||
@@ -3323,6 +3633,15 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -3458,6 +3777,12 @@ version = "0.3.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
@@ -4059,6 +4384,30 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.9.12+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap 2.13.0",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime 0.7.5+spec-1.1.0",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.7.5+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.0.1+spec-1.1.0"
|
version = "1.0.1+spec-1.1.0"
|
||||||
@@ -4075,9 +4424,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
|
checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.13.0",
|
"indexmap 2.13.0",
|
||||||
"toml_datetime",
|
"toml_datetime 1.0.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow",
|
"winnow 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4086,9 +4435,15 @@ version = "1.0.10+spec-1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
|
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.1.0+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tonic"
|
name = "tonic"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -4393,6 +4748,12 @@ version = "0.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unty"
|
||||||
|
version = "0.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -4491,6 +4852,12 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "virtue"
|
||||||
|
version = "0.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -5029,6 +5396,12 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -5281,3 +5654,31 @@ name = "zmij"
|
|||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd"
|
||||||
|
version = "0.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-safe",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-safe"
|
||||||
|
version = "7.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-sys"
|
||||||
|
version = "2.0.16+zstd.1.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|||||||
+22
-6
@@ -13,6 +13,7 @@ tokio = { version = "1", features = ["full"] }
|
|||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
|
libc = "0.2"
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
|
|
||||||
# CLI
|
# CLI
|
||||||
@@ -32,10 +33,14 @@ sha2 = "0.10"
|
|||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
uuid = { version = "1.0", features = ["v4"] }
|
uuid = { version = "1.0", features = ["v4"] }
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
subtle = "2.5"
|
subtle = "2.5"
|
||||||
aes-gcm = "0.10"
|
aes-gcm = "0.10"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
|
||||||
|
# Text processing
|
||||||
|
jieba-rs = "0.8.1"
|
||||||
|
ferrous-opencc = { version = "0.3.1", features = ["s2t-conversion", "t2s-conversion"] }
|
||||||
|
|
||||||
# Cache
|
# Cache
|
||||||
moka = { version = "0.12", features = ["future"] }
|
moka = { version = "0.12", features = ["future"] }
|
||||||
@@ -49,7 +54,7 @@ qdrant-client = "1.7"
|
|||||||
reqwest = { version = "0.12", features = ["json"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
|
||||||
# HTTP Server
|
# HTTP Server
|
||||||
axum = "0.7"
|
axum = { version = "0.7", features = ["multipart"] }
|
||||||
tower = "0.4"
|
tower = "0.4"
|
||||||
|
|
||||||
# API Documentation
|
# API Documentation
|
||||||
@@ -73,7 +78,6 @@ crossterm = "0.28"
|
|||||||
atty = "0.2"
|
atty = "0.2"
|
||||||
|
|
||||||
# System
|
# System
|
||||||
libc = "0.2"
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "momentry_core"
|
name = "momentry_core"
|
||||||
@@ -94,3 +98,15 @@ path = "src/player/main.rs"
|
|||||||
[[bin]]
|
[[bin]]
|
||||||
name = "momentry_playground"
|
name = "momentry_playground"
|
||||||
path = "src/playground.rs"
|
path = "src/playground.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fix_chunks"
|
||||||
|
path = "src/bin/fix_chunks.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "migrate_chinese_text"
|
||||||
|
path = "src/bin/migrate_chinese_text.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "test_bm25_simple"
|
||||||
|
path = "src/bin/test_bm25_simple.rs"
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.momentry.api</string>
|
||||||
|
|
||||||
|
<key>UserName</key>
|
||||||
|
<string>accusys</string>
|
||||||
|
|
||||||
|
<key>GroupName</key>
|
||||||
|
<string>staff</string>
|
||||||
|
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>/Users/accusys/momentry_core_0.1</string>
|
||||||
|
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/Users/accusys/momentry_core_0.1/target/release/momentry</string>
|
||||||
|
<string>server</string>
|
||||||
|
<string>--port</string>
|
||||||
|
<string>3002</string>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<key>PATH</key>
|
||||||
|
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||||
|
|
||||||
|
<key>DATABASE_URL</key>
|
||||||
|
<string>postgres://accusys@localhost:5432/momentry</string>
|
||||||
|
|
||||||
|
<key>DB_MAX_CONNECTIONS</key>
|
||||||
|
<string>50</string>
|
||||||
|
|
||||||
|
<key>DB_ACQUIRE_TIMEOUT</key>
|
||||||
|
<string>30</string>
|
||||||
|
|
||||||
|
<key>REDIS_URL</key>
|
||||||
|
<string>redis://:accusys@localhost:6379</string>
|
||||||
|
|
||||||
|
<key>REDIS_PASSWORD</key>
|
||||||
|
<string>accusys</string>
|
||||||
|
|
||||||
|
<key>OLLAMA_HOST</key>
|
||||||
|
<string>http://localhost:11434</string>
|
||||||
|
|
||||||
|
<key>QDRANT_URL</key>
|
||||||
|
<string>http://127.0.0.1:6333</string>
|
||||||
|
</dict>
|
||||||
|
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/Users/accusys/momentry/log/momentry_api.log</string>
|
||||||
|
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/Users/accusys/momentry/log/momentry_api.error.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
+45
-8
@@ -1,5 +1,23 @@
|
|||||||
# Momentry Core API 存取指南
|
# Momentry Core API 存取指南
|
||||||
|
|
||||||
|
| 項目 | 內容 |
|
||||||
|
|------|------|
|
||||||
|
| 版本 | V1.3 |
|
||||||
|
| 日期 | 2026-03-25 |
|
||||||
|
| 用途 | API 存取方式、端點與整合指南 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.3 | 2026-03-25 | 更新: n8n 搜尋回傳 `file_path` 取代 `media_url`,新增 API Key 驗證說明 | OpenCode | deepseek-reasoner |
|
||||||
|
| V1.2 | 2026-03-24 | 更新網址與服務列表 | Warren | OpenCode / MiniMax M2.5 |
|
||||||
|
| V1.1 | 2026-03-23 | 初始版本 | Warren | OpenCode / MiniMax M2.5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 基本網址
|
## 基本網址
|
||||||
|
|
||||||
| 環境 | URL | 說明 |
|
| 環境 | URL | 說明 |
|
||||||
@@ -20,7 +38,16 @@
|
|||||||
- 生產環境
|
- 生產環境
|
||||||
|
|
||||||
## 認證
|
## 認證
|
||||||
目前為開放狀態(示範用途無需認證)。正式環境將實作 API Key。
|
所有 `/api/v1/*` 端點(除了健康檢查 `/health` 與 `/health/detailed`)都需要 API Key 認證。
|
||||||
|
|
||||||
|
請在請求標頭中加入:
|
||||||
|
```
|
||||||
|
X-API-Key: YOUR_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
**目前示範使用的 API Key**: `demo_api_key_12345`
|
||||||
|
|
||||||
|
> **注意**: 正式環境請使用安全的 API Key 管理機制,避免在客戶端暴露 API Key。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -91,12 +118,14 @@
|
|||||||
"title": "Chunk sentence_0006",
|
"title": "Chunk sentence_0006",
|
||||||
"text": "fun plot twists...",
|
"text": "fun plot twists...",
|
||||||
"score": 0.526,
|
"score": 0.526,
|
||||||
"media_url": "https://wp.momentry.ddns.net/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
|
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **注意**: API 現在返回 `file_path`(檔案系統路徑)而非 `media_url`(網頁 URL)。如需在網頁中播放影片,請將檔案路徑轉換為可訪問的 URL(例如透過 SFTPGo 分享連結)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 影片管理 API
|
## 影片管理 API
|
||||||
@@ -134,7 +163,10 @@
|
|||||||
```javascript
|
```javascript
|
||||||
const response = await fetch('http://localhost:3002/api/v1/search', {
|
const response = await fetch('http://localhost:3002/api/v1/search', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-Key': 'YOUR_API_KEY' // 替換為實際的 API Key
|
||||||
|
},
|
||||||
body: JSON.stringify({ query: 'charade', limit: 5 })
|
body: JSON.stringify({ query: 'charade', limit: 5 })
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -149,7 +181,10 @@ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
|||||||
'query' => 'charade',
|
'query' => 'charade',
|
||||||
'limit' => 5
|
'limit' => 5
|
||||||
]));
|
]));
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'X-API-Key: YOUR_API_KEY' // 替換為實際的 API Key
|
||||||
|
]);
|
||||||
$response = curl_exec($ch);
|
$response = curl_exec($ch);
|
||||||
$data = json_decode($response, true);
|
$data = json_decode($response, true);
|
||||||
```
|
```
|
||||||
@@ -158,10 +193,12 @@ $data = json_decode($response, true);
|
|||||||
|
|
||||||
## 影片嵌入網址
|
## 影片嵌入網址
|
||||||
|
|
||||||
影片可透過 SFTPGo 分享連結存取:
|
> **重要**: API 現在返回 `file_path`(檔案系統路徑),而非直接可訪問的網址。您需要將檔案路徑轉換為 SFTPGo 分享連結才能嵌入影片。
|
||||||
```
|
|
||||||
https://wp.momentry.ddns.net/{檔案名稱}
|
**檔案路徑轉換為網址:**
|
||||||
```
|
- API 返回的 `file_path` 範例:`/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4`
|
||||||
|
- 對應的 SFTPGo 分享連結:`https://wp.momentry.ddns.net/demo/video.mp4`
|
||||||
|
- 轉換方式:移除 `/Users/accusys/momentry/var/sftpgo/data/` 前綴,將剩餘路徑附加到 `https://wp.momentry.ddns.net/`
|
||||||
|
|
||||||
**手動建立分享連結:**
|
**手動建立分享連結:**
|
||||||
1. 開啟 SFTPGo Web UI:`http://localhost:8080`
|
1. 開啟 SFTPGo Web UI:`http://localhost:8080`
|
||||||
|
|||||||
+105
-11
@@ -2,12 +2,23 @@
|
|||||||
|
|
||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 版本 | V1.2 |
|
| 版本 | V1.4 |
|
||||||
| 日期 | 2026-03-23 |
|
| 日期 | 2026-03-26 |
|
||||||
| Base URL | `http://localhost:3002` |
|
| Base URL | `http://localhost:3002` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.4 | 2026-03-26 | 新增: 任務管理端點 (`/api/v1/jobs`, `/api/v1/jobs/:uuid`),更新註冊端點回應格式 | OpenCode | deepseek-reasoner |
|
||||||
|
| V1.3 | 2026-03-25 | 更新: n8n 搜尋回傳 `file_path` 取代 `media_url`,新增 API Key 驗證說明 | OpenCode | deepseek-reasoner |
|
||||||
|
| V1.2 | 2026-03-23 | 建立 curl 範例文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||||
|
| V1.1 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
> **狀態說明**:
|
> **狀態說明**:
|
||||||
> - ✅ **已實作**: 健康檢查、搜尋、影片管理端點
|
> - ✅ **已實作**: 健康檢查、搜尋、影片管理端點
|
||||||
> - ⚠️ **規劃中**: API Key 管理功能
|
> - ⚠️ **規劃中**: API Key 管理功能
|
||||||
@@ -76,6 +87,20 @@ sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## API 認證
|
||||||
|
|
||||||
|
所有 `/api/v1/*` 端點(除了健康檢查)都需要 API Key 認證。請在請求標頭中加入:
|
||||||
|
|
||||||
|
```
|
||||||
|
-H "X-API-Key: YOUR_API_KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
**目前示範使用的 API Key**: `demo_api_key_12345`
|
||||||
|
|
||||||
|
> **注意**: 正式環境請使用安全的 API Key 管理機制。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 1. 已實作端點
|
## 1. 已實作端點
|
||||||
|
|
||||||
### 健康檢查
|
### 健康檢查
|
||||||
@@ -161,6 +186,7 @@ curl -X GET http://localhost:3002/api/v1/api-keys/stats \
|
|||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3002/api/v1/register \
|
curl -X POST http://localhost:3002/api/v1/register \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"path": "/path/to/video.mp4"}'
|
-d '{"path": "/path/to/video.mp4"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -168,30 +194,31 @@ curl -X POST http://localhost:3002/api/v1/register \
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": 1,
|
|
||||||
"uuid": "a1b2c3d4e5f6g7h8",
|
"uuid": "a1b2c3d4e5f6g7h8",
|
||||||
"file_path": "/path/to/video.mp4",
|
"video_id": 1,
|
||||||
|
"job_id": 123,
|
||||||
"file_name": "video.mp4",
|
"file_name": "video.mp4",
|
||||||
"duration": 120.5,
|
"duration": 120.5,
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080,
|
||||||
|
"already_exists": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.2 列出所有影片 ✅
|
### 3.2 列出所有影片 ✅
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:3002/api/v1/videos
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.3 查詢影片 ✅
|
### 3.3 查詢影片 ✅
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 依 UUID 查詢
|
# 依 UUID 查詢
|
||||||
curl "http://localhost:3002/api/v1/lookup?uuid=a1b2c3d4e5f6g7h8"
|
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?uuid=a1b2c3d4e5f6g7h8"
|
||||||
|
|
||||||
# 依路徑查詢
|
# 依路徑查詢
|
||||||
curl "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4"
|
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.4 處理影片 🔧 *(CLI - 非 API)*
|
### 3.4 處理影片 🔧 *(CLI - 非 API)*
|
||||||
@@ -209,7 +236,7 @@ cargo run --bin momentry -- process <uuid1> <uuid2> <uuid3>
|
|||||||
### 3.5 取得處理進度 ✅
|
### 3.5 取得處理進度 ✅
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:3002/api/v1/progress/<uuid>
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/progress/<uuid>
|
||||||
```
|
```
|
||||||
|
|
||||||
**回應範例**:
|
**回應範例**:
|
||||||
@@ -247,6 +274,67 @@ curl http://localhost:3002/api/v1/progress/<uuid>
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 3.6 任務管理 ✅
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 列出所有任務
|
||||||
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/jobs
|
||||||
|
|
||||||
|
# 取得特定任務詳情
|
||||||
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/jobs/<uuid>
|
||||||
|
```
|
||||||
|
|
||||||
|
**任務列表回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jobs": [
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"uuid": "a1b2c3d4e5f6g7h8",
|
||||||
|
"status": "pending",
|
||||||
|
"current_processor": null,
|
||||||
|
"progress_current": 0,
|
||||||
|
"progress_total": 100,
|
||||||
|
"created_at": "2026-03-26 10:30:00",
|
||||||
|
"started_at": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**任務詳情回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"uuid": "a1b2c3d4e5f6g7h8",
|
||||||
|
"status": "processing",
|
||||||
|
"current_processor": "asr",
|
||||||
|
"progress_current": 50,
|
||||||
|
"progress_total": 100,
|
||||||
|
"processors": [
|
||||||
|
{
|
||||||
|
"processor_type": "asr",
|
||||||
|
"status": "complete",
|
||||||
|
"started_at": "2026-03-26 10:30:00",
|
||||||
|
"completed_at": "2026-03-26 10:35:00",
|
||||||
|
"duration_secs": 300.5,
|
||||||
|
"error_message": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"processor_type": "cut",
|
||||||
|
"status": "pending",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"duration_secs": null,
|
||||||
|
"error_message": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"created_at": "2026-03-26 10:30:00",
|
||||||
|
"started_at": "2026-03-26 10:30:00",
|
||||||
|
"updated_at": "2026-03-26 10:35:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 查詢與搜索
|
## 4. 查詢與搜索
|
||||||
@@ -256,6 +344,7 @@ curl http://localhost:3002/api/v1/progress/<uuid>
|
|||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3002/api/v1/search \
|
curl -X POST http://localhost:3002/api/v1/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{
|
-d '{
|
||||||
"query": "測試關鍵字",
|
"query": "測試關鍵字",
|
||||||
"limit": 5
|
"limit": 5
|
||||||
@@ -286,6 +375,7 @@ curl -X POST http://localhost:3002/api/v1/search \
|
|||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{
|
-d '{
|
||||||
"query": "測試關鍵字",
|
"query": "測試關鍵字",
|
||||||
"limit": 5
|
"limit": 5
|
||||||
@@ -307,7 +397,7 @@ curl -X POST http://localhost:3002/api/v1/n8n/search \
|
|||||||
"title": "Chunk sentence_0006",
|
"title": "Chunk sentence_0006",
|
||||||
"text": "fun plot twists...",
|
"text": "fun plot twists...",
|
||||||
"score": 0.92,
|
"score": 0.92,
|
||||||
"media_url": "https://wp.momentry.ddns.net/video.mp4"
|
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -318,6 +408,7 @@ curl -X POST http://localhost:3002/api/v1/n8n/search \
|
|||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3002/api/v1/search/hybrid \
|
curl -X POST http://localhost:3002/api/v1/search/hybrid \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{
|
-d '{
|
||||||
"query": "測試關鍵字",
|
"query": "測試關鍵字",
|
||||||
"limit": 5
|
"limit": 5
|
||||||
@@ -425,6 +516,8 @@ A: 需要將工作流程切換為 Active 狀態 (右上角開關)
|
|||||||
| `/api/v1/lookup` | GET | ✅ | 查詢影片 |
|
| `/api/v1/lookup` | GET | ✅ | 查詢影片 |
|
||||||
| `/api/v1/videos` | GET | ✅ | 列出所有影片 |
|
| `/api/v1/videos` | GET | ✅ | 列出所有影片 |
|
||||||
| `/api/v1/progress/:uuid` | GET | ✅ | 處理進度 |
|
| `/api/v1/progress/:uuid` | GET | ✅ | 處理進度 |
|
||||||
|
| `/api/v1/jobs` | GET | ✅ | 任務列表 |
|
||||||
|
| `/api/v1/jobs/:uuid` | GET | ✅ | 任務詳情 |
|
||||||
| `/api/v1/api-keys` | * | ⚠️ | API Key 管理 (規劃中) |
|
| `/api/v1/api-keys` | * | ⚠️ | API Key 管理 (規劃中) |
|
||||||
|
|
||||||
### C. 常見錯誤
|
### C. 常見錯誤
|
||||||
@@ -475,11 +568,12 @@ curl -s "$API_URL/health" | jq .
|
|||||||
echo -e "\n=== Search ==="
|
echo -e "\n=== Search ==="
|
||||||
curl -s -X POST "$API_URL/api/v1/search" \
|
curl -s -X POST "$API_URL/api/v1/search" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"query": "test", "limit": 3}' | jq .
|
-d '{"query": "test", "limit": 3}' | jq .
|
||||||
|
|
||||||
# 列出影片
|
# 列出影片
|
||||||
echo -e "\n=== Videos ==="
|
echo -e "\n=== Videos ==="
|
||||||
curl -s "$API_URL/api/v1/videos" | jq '.videos | length'
|
curl -s -H "X-API-Key: YOUR_API_KEY" "$API_URL/api/v1/videos" | jq '.videos | length'
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+83
-6
@@ -2,8 +2,20 @@
|
|||||||
|
|
||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 版本 | V1.1 |
|
| 建立者 | Warren |
|
||||||
| 日期 | 2026-03-25 |
|
| 建立時間 | 2026-03-18 |
|
||||||
|
| 文件版本 | V1.3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| V1.0 | 2026-03-18 | 創建文件 | OpenCode |
|
||||||
|
| V1.1 | 2026-03-23 | 更新端點與實際一致 | OpenCode |
|
||||||
|
| V1.2 | 2026-03-25 | 新增快取/刪除 API | OpenCode |
|
||||||
|
| V1.3 | 2026-03-26 | 更新API回應格式 (media_url→file_path) | OpenCode |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -70,6 +82,7 @@ curl http://localhost:3002/health
|
|||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3002/api/v1/search \
|
curl -X POST http://localhost:3002/api/v1/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-api-key" \
|
||||||
-d '{"query": "test", "limit": 10}'
|
-d '{"query": "test", "limit": 10}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -77,6 +90,7 @@ curl -X POST http://localhost:3002/api/v1/search \
|
|||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-api-key" \
|
||||||
-d '{"query": "test", "limit": 10}'
|
-d '{"query": "test", "limit": 10}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -96,13 +110,29 @@ curl -X POST http://localhost:3002/api/v1/n8n/search \
|
|||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3002/api/v1/register \
|
curl -X POST http://localhost:3002/api/v1/register \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-api-key" \
|
||||||
-d '{"path": "/path/to/video.mp4"}'
|
-d '{"path": "/path/to/video.mp4"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**註冊回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "a1b10138a6bbb0cd",
|
||||||
|
"video_id": 1,
|
||||||
|
"job_id": 10,
|
||||||
|
"file_name": "video.mp4",
|
||||||
|
"duration": 120.5,
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080,
|
||||||
|
"already_exists": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**探測影片** (不註冊,只取得影片資訊):
|
**探測影片** (不註冊,只取得影片資訊):
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3002/api/v1/probe \
|
curl -X POST http://localhost:3002/api/v1/probe \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-api-key" \
|
||||||
-d '{"path": "./demo/video.mp4"}'
|
-d '{"path": "./demo/video.mp4"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -139,17 +169,61 @@ curl -X POST http://localhost:3002/api/v1/probe \
|
|||||||
|
|
||||||
**列出影片**:
|
**列出影片**:
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:3002/api/v1/videos
|
curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/videos
|
||||||
```
|
```
|
||||||
|
|
||||||
**查詢影片**:
|
**查詢影片**:
|
||||||
```bash
|
```bash
|
||||||
curl "http://localhost:3002/api/v1/lookup?uuid=5dea6618a606e7c7"
|
curl -H "X-API-Key: your-api-key" "http://localhost:3002/api/v1/lookup?uuid=5dea6618a606e7c7"
|
||||||
```
|
```
|
||||||
|
|
||||||
**處理進度**:
|
**處理進度**:
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:3002/api/v1/progress/5dea6618a606e7c7
|
curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/progress/5dea6618a606e7c7
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 工作管理
|
||||||
|
|
||||||
|
| 方法 | 端點 | 說明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | `/api/v1/jobs` | 列出所有工作 |
|
||||||
|
| GET | `/api/v1/jobs/:uuid` | 取得指定工作的詳細資訊 |
|
||||||
|
|
||||||
|
**列出工作**:
|
||||||
|
```bash
|
||||||
|
curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/jobs
|
||||||
|
```
|
||||||
|
|
||||||
|
**取得工作詳細資訊**:
|
||||||
|
```bash
|
||||||
|
curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/jobs/a03485a40b2df2d3
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 系統管理
|
||||||
|
|
||||||
|
| 方法 | 端點 | 說明 |
|
||||||
|
|------|------|------|
|
||||||
|
| POST | `/api/v1/config/cache` | 切換快取功能(管理員) |
|
||||||
|
| POST | `/api/v1/unregister` | 刪除影片及其所有資料(管理員) |
|
||||||
|
|
||||||
|
**快取設定**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/v1/config/cache \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-api-key" \
|
||||||
|
-d '{"enabled": true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**刪除影片**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/v1/unregister \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-api-key" \
|
||||||
|
-d '{"uuid": "5dea6618a606e7c7"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -165,6 +239,9 @@ curl http://localhost:3002/api/v1/progress/5dea6618a606e7c7
|
|||||||
| 列出影片 | ✓ | ✓ | ✓ |
|
| 列出影片 | ✓ | ✓ | ✓ |
|
||||||
| 查詢影片 | ✓ | ✓ | ✓ |
|
| 查詢影片 | ✓ | ✓ | ✓ |
|
||||||
| 處理進度 | ✓ | ✓ | ✓ |
|
| 處理進度 | ✓ | ✓ | ✓ |
|
||||||
|
| 工作管理 | ✓ | ✓ | ✓ |
|
||||||
|
| 快取設定 | ✓ (管理員) | ✓ (管理員) | ✓ (管理員) |
|
||||||
|
| 刪除影片 | ✓ (管理員) | ✓ (管理員) | ✓ (管理員) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -184,7 +261,7 @@ curl http://localhost:3002/api/v1/progress/5dea6618a606e7c7
|
|||||||
"title": "Chunk sentence_0001",
|
"title": "Chunk sentence_0001",
|
||||||
"text": "...",
|
"text": "...",
|
||||||
"score": 0.92,
|
"score": 0.92,
|
||||||
"media_url": "https://wp.momentry.ddns.net/video.mp4"
|
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+62
-17
@@ -2,13 +2,22 @@
|
|||||||
|
|
||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 版本 | V2.0 |
|
| 版本 | V2.1 |
|
||||||
| 日期 | 2026-03-25 |
|
| 日期 | 2026-03-26 |
|
||||||
| Base URL (本地) | `http://localhost:3002` |
|
| Base URL (本地) | `http://localhost:3002` |
|
||||||
| Base URL (外部) | `https://api.momentry.ddns.net` |
|
| Base URL (外部) | `https://api.momentry.ddns.net` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| V2.0 | 2026-03-25 | 創建完整範例總覽 | OpenCode |
|
||||||
|
| V2.1 | 2026-03-26 | 更新API回應格式 (media_url→file_path) 與認證標頭 | OpenCode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 快速參考
|
## 快速參考
|
||||||
|
|
||||||
### 環境 URL 選擇
|
### 環境 URL 選擇
|
||||||
@@ -105,16 +114,19 @@ curl http://localhost:3002/health/detailed
|
|||||||
# 標準格式搜尋
|
# 標準格式搜尋
|
||||||
curl -X POST http://localhost:3002/api/v1/search \
|
curl -X POST http://localhost:3002/api/v1/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"query": "charade", "limit": 5}'
|
-d '{"query": "charade", "limit": 5}'
|
||||||
|
|
||||||
# n8n 格式搜尋(推薦)
|
# n8n 格式搜尋(推薦)
|
||||||
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"query": "charade", "limit": 5}'
|
-d '{"query": "charade", "limit": 5}'
|
||||||
|
|
||||||
# 混合搜尋
|
# 混合搜尋
|
||||||
curl -X POST http://localhost:3002/api/v1/search/hybrid \
|
curl -X POST http://localhost:3002/api/v1/search/hybrid \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"query": "charade", "limit": 5}'
|
-d '{"query": "charade", "limit": 5}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -150,7 +162,7 @@ curl -X POST http://localhost:3002/api/v1/search/hybrid \
|
|||||||
"title": "Chunk sentence_0001",
|
"title": "Chunk sentence_0001",
|
||||||
"text": "fun plot twists...",
|
"text": "fun plot twists...",
|
||||||
"score": 0.92,
|
"score": 0.92,
|
||||||
"media_url": "https://wp.momentry.ddns.net/video.mp4"
|
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -160,26 +172,28 @@ curl -X POST http://localhost:3002/api/v1/search/hybrid \
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 列出所有影片
|
# 列出所有影片
|
||||||
curl http://localhost:3002/api/v1/videos
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||||
|
|
||||||
# 查詢特定影片(依 UUID)
|
# 查詢特定影片(依 UUID)
|
||||||
curl "http://localhost:3002/api/v1/lookup?uuid=a1b10138a6bbb0cd"
|
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?uuid=a1b10138a6bbb0cd"
|
||||||
|
|
||||||
# 查詢特定影片(依路徑)
|
# 查詢特定影片(依路徑)
|
||||||
curl "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4"
|
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4"
|
||||||
|
|
||||||
# 取得處理進度
|
# 取得處理進度
|
||||||
curl http://localhost:3002/api/v1/progress/a1b10138a6bbb0cd
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/progress/a1b10138a6bbb0cd
|
||||||
|
|
||||||
# 探測影片(不註冊)
|
# 探測影片(不註冊)
|
||||||
curl -X POST http://localhost:3002/api/v1/probe \
|
curl -X POST http://localhost:3002/api/v1/probe \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"path": "/path/to/video.mp4"}'
|
-d '{"path": "/path/to/video.mp4"}'
|
||||||
|
|
||||||
# 註冊影片
|
# 註冊影片
|
||||||
curl -X POST http://localhost:3002/api/v1/register \
|
curl -X POST http://localhost:3002/api/v1/register \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"path": "/path/to/video.mp4", "file_name": "video.mp4"}'
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-d '{"path": "/path/to/video.mp4"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1.4 批次測試腳本
|
### 1.4 批次測試腳本
|
||||||
@@ -196,10 +210,11 @@ curl -s "$API_URL/health" | jq .
|
|||||||
echo -e "\n=== 語意搜尋 ==="
|
echo -e "\n=== 語意搜尋 ==="
|
||||||
curl -s -X POST "$API_URL/api/v1/search" \
|
curl -s -X POST "$API_URL/api/v1/search" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"query": "charade", "limit": 3}' | jq .
|
-d '{"query": "charade", "limit": 3}' | jq .
|
||||||
|
|
||||||
echo -e "\n=== 影片列表 ==="
|
echo -e "\n=== 影片列表 ==="
|
||||||
curl -s "$API_URL/api/v1/videos" | jq '.videos | length'
|
curl -s -H "X-API-Key: YOUR_API_KEY" "$API_URL/api/v1/videos" | jq '.videos | length'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1.5 外部 URL 範例
|
### 1.5 外部 URL 範例
|
||||||
@@ -211,6 +226,7 @@ curl https://api.momentry.ddns.net/health
|
|||||||
# 外部搜尋
|
# 外部搜尋
|
||||||
curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \
|
curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"query": "charade", "limit": 5}'
|
-d '{"query": "charade", "limit": 5}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -227,11 +243,14 @@ Node: HTTP Request
|
|||||||
├── Authentication: None
|
├── Authentication: None
|
||||||
├── Send Body: ✓ (checked)
|
├── Send Body: ✓ (checked)
|
||||||
├── Content Type: JSON
|
├── Content Type: JSON
|
||||||
└── Body:
|
├── Body:
|
||||||
{
|
│ {
|
||||||
"query": "={{ $json.query }}",
|
│ "query": "={{ $json.query }}",
|
||||||
"limit": "={{ $json.limit || 10 }}"
|
│ "limit": "={{ $json.limit || 10 }}"
|
||||||
}
|
│ }
|
||||||
|
├── Send Headers: ✓ (checked)
|
||||||
|
└── Header Parameters:
|
||||||
|
└── X-API-Key: {{ $env.MOMENTRY_API_KEY }}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.2 基本搜尋 Workflow
|
### 2.2 基本搜尋 Workflow
|
||||||
@@ -460,6 +479,24 @@ searchVideos('charade', 5)
|
|||||||
|
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php
|
||||||
|
// 將文件路徑轉換為可訪問的 URL
|
||||||
|
function convert_file_path_to_url($file_path) {
|
||||||
|
// 範例: 將 SFTPGo 文件路徑轉換為 web URL
|
||||||
|
// /Users/accusys/momentry/var/sftpgo/data/demo/video.mp4
|
||||||
|
// → https://sftpgo.example.com/demo/video.mp4
|
||||||
|
|
||||||
|
// 移除基本路徑
|
||||||
|
$base_path = '/Users/accusys/momentry/var/sftpgo/data/';
|
||||||
|
if (strpos($file_path, $base_path) === 0) {
|
||||||
|
$relative_path = substr($file_path, strlen($base_path));
|
||||||
|
// 替換為實際的 SFTPGo web URL
|
||||||
|
return 'https://sftpgo.example.com/' . $relative_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果無法轉換,返回原始路徑
|
||||||
|
return $file_path;
|
||||||
|
}
|
||||||
|
|
||||||
// 註冊短碼
|
// 註冊短碼
|
||||||
add_shortcode('momentry_search', function($atts) {
|
add_shortcode('momentry_search', function($atts) {
|
||||||
$atts = shortcode_atts([
|
$atts = shortcode_atts([
|
||||||
@@ -472,7 +509,10 @@ add_shortcode('momentry_search', function($atts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$response = wp_remote_post('https://api.momentry.ddns.net/api/v1/n8n/search', [
|
$response = wp_remote_post('https://api.momentry.ddns.net/api/v1/n8n/search', [
|
||||||
'headers' => ['Content-Type' => 'application/json'],
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'X-API-Key' => 'YOUR_API_KEY' // 替換為實際的 API Key
|
||||||
|
],
|
||||||
'body' => json_encode([
|
'body' => json_encode([
|
||||||
'query' => $atts['query'],
|
'query' => $atts['query'],
|
||||||
'limit' => (int)$atts['limit']
|
'limit' => (int)$atts['limit']
|
||||||
@@ -492,10 +532,15 @@ add_shortcode('momentry_search', function($atts) {
|
|||||||
|
|
||||||
$output = '<ul class="momentry-results">';
|
$output = '<ul class="momentry-results">';
|
||||||
foreach ($data['hits'] as $hit) {
|
foreach ($data['hits'] as $hit) {
|
||||||
|
// 注意: API 現在返回 file_path 而非 media_url
|
||||||
|
// 需要將文件路徑轉換為可訪問的 URL
|
||||||
|
$file_path = $hit['file_path'];
|
||||||
|
$video_url = convert_file_path_to_url($file_path); // 需要實作此函數
|
||||||
|
|
||||||
$output .= sprintf(
|
$output .= sprintf(
|
||||||
'<li>%s <a href="%s?start=%s">播放</a></li>',
|
'<li>%s <a href="%s?start=%s">播放</a></li>',
|
||||||
esc_html($hit['text']),
|
esc_html($hit['text']),
|
||||||
$hit['media_url'],
|
$video_url,
|
||||||
$hit['start']
|
$hit['start']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -569,7 +614,7 @@ Body: {"query": "charade", "limit": 5}
|
|||||||
"title": "Chunk sentence_0001",
|
"title": "Chunk sentence_0001",
|
||||||
"text": "fun plot twists...",
|
"text": "fun plot twists...",
|
||||||
"score": 0.92,
|
"score": 0.92,
|
||||||
"media_url": "https://wp.momentry.ddns.net/video.mp4"
|
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -2,8 +2,19 @@
|
|||||||
|
|
||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 版本 | V2.1 |
|
| 建立者 | OpenCode |
|
||||||
| 日期 | 2026-03-25 |
|
| 建立時間 | 2026-03-25 |
|
||||||
|
| 文件版本 | V2.2 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V2.0 | 2026-03-22 | 創建 API 文件總覽 | Warren | OpenCode |
|
||||||
|
| V2.1 | 2026-03-24 | 新增文件分類與快速選擇指南 | OpenCode | deepseek-reasoner |
|
||||||
|
| V2.2 | 2026-03-25 | 更新 API Key 驗證說明與文件連結 | OpenCode | deepseek-reasoner |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,11 +25,15 @@ docs/
|
|||||||
├── API_INDEX.md ← 本文件:總覽與入口
|
├── API_INDEX.md ← 本文件:總覽與入口
|
||||||
├── API_ENDPOINTS.md ← API 端點完整說明
|
├── API_ENDPOINTS.md ← API 端點完整說明
|
||||||
├── API_EXAMPLES.md ← 完整範例總覽(curl / n8n / WordPress)
|
├── API_EXAMPLES.md ← 完整範例總覽(curl / n8n / WordPress)
|
||||||
|
├── API_REFERENCE.md ← 詳細技術參考
|
||||||
├── DEMO_MANUAL.md ← ⭐ 示範手冊(含 Demo API Key)
|
├── DEMO_MANUAL.md ← ⭐ 示範手冊(含 Demo API Key)
|
||||||
├── API_N8N_GUIDE.md ← n8n 詳細指南
|
├── API_N8N_GUIDE.md ← n8n 詳細指南
|
||||||
├── API_WORDPRESS_GUIDE.md ← WordPress 詳細指南
|
├── API_WORDPRESS_GUIDE.md ← WordPress 詳細指南
|
||||||
├── API_CURL_EXAMPLES.md ← curl 快速範例
|
├── API_CURL_EXAMPLES.md ← curl 快速範例
|
||||||
└── API_REFERENCE.md ← 詳細技術參考
|
│
|
||||||
|
├── API_TRAINING_MARCOM.md ← ⭐ marcom 團隊教育訓練手冊
|
||||||
|
├── API_WORKFLOW_WORDPRESS_N8N.md ← WordPress/n8n 完整工作流程
|
||||||
|
└── CHUNK_DATA_STRUCTURE.md ← Chunk 資料結構說明
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -29,7 +44,9 @@ docs/
|
|||||||
|------|----------|
|
|------|----------|
|
||||||
| **我要快速開始測試** | ⭐ [DEMO_MANUAL.md](./DEMO_MANUAL.md) |
|
| **我要快速開始測試** | ⭐ [DEMO_MANUAL.md](./DEMO_MANUAL.md) |
|
||||||
| **我要查看所有範例** | [API_EXAMPLES.md](./API_EXAMPLES.md) |
|
| **我要查看所有範例** | [API_EXAMPLES.md](./API_EXAMPLES.md) |
|
||||||
|
| **我是 marcom 團隊** | ⭐ [API_TRAINING_MARCOM.md](./API_TRAINING_MARCOM.md) |
|
||||||
| 我想了解有哪些 API 端點 | [API_ENDPOINTS.md](./API_ENDPOINTS.md) |
|
| 我想了解有哪些 API 端點 | [API_ENDPOINTS.md](./API_ENDPOINTS.md) |
|
||||||
|
| 我要整合 WordPress/n8n | [API_WORKFLOW_WORDPRESS_N8N.md](./API_WORKFLOW_WORDPRESS_N8N.md) |
|
||||||
| 我要在 n8n workflow 中呼叫 API | [DEMO_MANUAL.md](./DEMO_MANUAL.md#2-n8n-範例) |
|
| 我要在 n8n workflow 中呼叫 API | [DEMO_MANUAL.md](./DEMO_MANUAL.md#2-n8n-範例) |
|
||||||
| 我要在 WordPress 中呼叫 API | [DEMO_MANUAL.md](./DEMO_MANUAL.md#3-wordpress-範例) |
|
| 我要在 WordPress 中呼叫 API | [DEMO_MANUAL.md](./DEMO_MANUAL.md#3-wordpress-範例) |
|
||||||
| 我要用 curl 快速測試 | [DEMO_MANUAL.md](./DEMO_MANUAL.md#1-curl-範例) |
|
| 我要用 curl 快速測試 | [DEMO_MANUAL.md](./DEMO_MANUAL.md#1-curl-範例) |
|
||||||
|
|||||||
@@ -2,9 +2,23 @@
|
|||||||
|
|
||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 版本 | V1.2 |
|
| 建立者 | Warren |
|
||||||
| 日期 | 2026-03-21 |
|
| 建立時間 | 2026-03-21 |
|
||||||
| 狀態 | 開發中 |
|
| 文件版本 | V1.2 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||||
|
| V1.1 | 2026-03-20 | 新增 Key 類型與管理流程 | Warren | OpenCode |
|
||||||
|
| V1.2 | 2026-03-21 | 更新 API Key 格式與驗證流程 | Warren | OpenCode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**狀態**: 開發中
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+43
-14
@@ -2,9 +2,22 @@
|
|||||||
|
|
||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 版本 | V1.0 |
|
| 建立者 | Warren |
|
||||||
| 日期 | 2026-03-23 |
|
| 建立時間 | 2026-03-23 |
|
||||||
| 用途 | 在 n8n workflow 中呼叫 Momentry API |
|
| 文件版本 | V1.1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-23 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||||
|
| V1.1 | 2026-03-26 | 新增 API Key 驗證說明,更新 HTTP Request Node 設定 | OpenCode | deepseek-reasoner |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**用途**: 在 n8n workflow 中呼叫 Momentry API
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -29,6 +42,8 @@ https://api.momentry.ddns.net
|
|||||||
| GET | `/api/v1/videos` | 列出所有影片 |
|
| GET | `/api/v1/videos` | 列出所有影片 |
|
||||||
| GET | `/api/v1/lookup` | 查詢影片 |
|
| GET | `/api/v1/lookup` | 查詢影片 |
|
||||||
| GET | `/api/v1/progress/:uuid` | 處理進度 |
|
| GET | `/api/v1/progress/:uuid` | 處理進度 |
|
||||||
|
| GET | `/api/v1/jobs` | 任務列表 |
|
||||||
|
| GET | `/api/v1/jobs/:uuid` | 任務詳情 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -43,11 +58,14 @@ Node: HTTP Request
|
|||||||
├── Authentication: None
|
├── Authentication: None
|
||||||
├── Send Body: ✓ (checked)
|
├── Send Body: ✓ (checked)
|
||||||
├── Content Type: JSON
|
├── Content Type: JSON
|
||||||
└── Body:
|
├── Body:
|
||||||
{
|
│ {
|
||||||
"query": "={{ $json.query }}",
|
│ "query": "={{ $json.query }}",
|
||||||
"limit": "={{ $json.limit || 10 }}"
|
│ "limit": "={{ $json.limit || 10 }}"
|
||||||
}
|
│ }
|
||||||
|
├── Send Headers: ✓ (checked)
|
||||||
|
└── Header Parameters:
|
||||||
|
└── X-API-Key: {{ $env.MOMENTRY_API_KEY }}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 測試用(固定關鍵字)
|
### 測試用(固定關鍵字)
|
||||||
@@ -58,11 +76,14 @@ Node: HTTP Request
|
|||||||
├── Method: POST
|
├── Method: POST
|
||||||
├── Send Body: ✓
|
├── Send Body: ✓
|
||||||
├── Content Type: JSON
|
├── Content Type: JSON
|
||||||
└── Body:
|
├── Body:
|
||||||
{
|
│ {
|
||||||
"query": "charade",
|
│ "query": "charade",
|
||||||
"limit": 3
|
│ "limit": 3
|
||||||
}
|
│ }
|
||||||
|
├── Send Headers: ✓ (checked)
|
||||||
|
└── Header Parameters:
|
||||||
|
└── X-API-Key: {{ $env.MOMENTRY_API_KEY }}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -174,13 +195,21 @@ sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
|||||||
|
|
||||||
在終端機中測試 API:
|
在終端機中測試 API:
|
||||||
|
|
||||||
|
> **注意**: 所有 `/api/v1/*` 端點都需要 API Key 驗證。請設定環境變數或直接替換 API Key。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 設定環境變數(使用您的 API Key)
|
||||||
|
export MOMENTRY_API_KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 健康檢查
|
# 健康檢查
|
||||||
curl https://api.momentry.ddns.net/health
|
curl https://api.momentry.ddns.net/health
|
||||||
|
|
||||||
# 搜尋測試
|
# 搜尋測試 (需要 API Key)
|
||||||
curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \
|
curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: $MOMENTRY_API_KEY" \
|
||||||
-d '{"query":"charade","limit":3}'
|
-d '{"query":"charade","limit":3}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,532 @@
|
|||||||
|
# Momentry Core API 快速查詢表
|
||||||
|
|
||||||
|
| 版本 | 日期 | 建立者 |
|
||||||
|
|------|------|--------|
|
||||||
|
| V1.0 | 2026-03-26 | OpenCode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 快速導覽
|
||||||
|
|
||||||
|
| 類別 | 端點數量 | 說明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 健康檢查 | 2 | 系統狀態監控 |
|
||||||
|
| 影片管理 | 5 | 影片註冊、查詢、刪除 |
|
||||||
|
| 搜尋功能 | 3 | 語意搜尋、混合搜尋 |
|
||||||
|
| 任務管理 | 2 | 處理任務狀態查詢 |
|
||||||
|
| 系統管理 | 2 | 快取設定、進度查詢 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 認證
|
||||||
|
|
||||||
|
所有 `/api/v1/*` 端點需要 `X-API-Key` header:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "X-API-Key: YOUR_API_KEY" ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**公開端點(無需認證):**
|
||||||
|
- `GET /health`
|
||||||
|
- `GET /health/detailed`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 端點總表
|
||||||
|
|
||||||
|
### 健康檢查
|
||||||
|
|
||||||
|
| 方法 | 端點 | 認證 | 描述 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| GET | `/health` | 公開 | 基本健康檢查 |
|
||||||
|
| GET | `/health/detailed` | 公開 | 詳細健康檢查(包含所有服務狀態) |
|
||||||
|
|
||||||
|
### 影片管理
|
||||||
|
|
||||||
|
| 方法 | 端點 | 認證 | 描述 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| POST | `/api/v1/register` | 需要 | 註冊影片並開始處理 |
|
||||||
|
| POST | `/api/v1/unregister` | 需要 | 刪除影片及其所有資料 |
|
||||||
|
| POST | `/api/v1/probe` | 需要 | 探測影片資訊(不註冊) |
|
||||||
|
| GET | `/api/v1/videos` | 需要 | 列出所有已註冊影片 |
|
||||||
|
| GET | `/api/v1/lookup` | 需要 | 查詢影片資訊 |
|
||||||
|
|
||||||
|
### 搜尋功能
|
||||||
|
|
||||||
|
| 方法 | 端點 | 認證 | 描述 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| POST | `/api/v1/search` | 需要 | 語意搜尋(標準格式) |
|
||||||
|
| POST | `/api/v1/n8n/search` | 需要 | 語意搜尋(n8n 格式) |
|
||||||
|
| POST | `/api/v1/search/hybrid` | 需要 | 混合搜尋(向量 + 關鍵字) |
|
||||||
|
|
||||||
|
### 任務管理
|
||||||
|
|
||||||
|
| 方法 | 端點 | 認證 | 描述 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| GET | `/api/v1/jobs` | 需要 | 列出所有處理任務 |
|
||||||
|
| GET | `/api/v1/jobs/:uuid` | 需要 | 取得特定任務詳情 |
|
||||||
|
|
||||||
|
### 系統管理
|
||||||
|
|
||||||
|
| 方法 | 端點 | 認證 | 描述 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| GET | `/api/v1/progress/:uuid` | 需要 | 取得影片處理進度 |
|
||||||
|
| POST | `/api/v1/config/cache` | 需要 | 切換快取功能 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 詳細端點說明
|
||||||
|
|
||||||
|
### 1. 健康檢查
|
||||||
|
|
||||||
|
#### GET /health
|
||||||
|
**基本健康檢查**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3002/health
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"uptime_ms": 14426558
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /health/detailed
|
||||||
|
**詳細健康檢查**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3002/health/detailed
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"uptime_ms": 14441362,
|
||||||
|
"services": {
|
||||||
|
"postgres": {"status": "ok", "latency_ms": 50, "error": null},
|
||||||
|
"redis": {"status": "ok", "latency_ms": 0, "error": null},
|
||||||
|
"qdrant": {"status": "ok", "latency_ms": 2, "error": null},
|
||||||
|
"mongodb": {"status": "ok", "latency_ms": 2, "error": null}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 影片管理
|
||||||
|
|
||||||
|
#### POST /api/v1/register
|
||||||
|
**註冊影片並開始處理**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/v1/register \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-d '{"path": "/path/to/video.mp4"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**請求:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"path": "/path/to/video.mp4"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "5dea6618a606e7c7",
|
||||||
|
"video_id": 10,
|
||||||
|
"job_id": 1,
|
||||||
|
"file_name": "video.mp4",
|
||||||
|
"duration": 596.458333,
|
||||||
|
"width": 320,
|
||||||
|
"height": 180,
|
||||||
|
"already_exists": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/v1/unregister
|
||||||
|
**刪除影片及其所有資料**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/v1/unregister \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-d '{"uuid": "5dea6618a606e7c7"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**請求:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "5dea6618a606e7c7"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"uuid": "5dea6618a606e7c7",
|
||||||
|
"message": "Video unregistered successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/v1/probe
|
||||||
|
**探測影片資訊(不註冊)**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/v1/probe \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-d '{"path": "/path/to/video.mp4"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**請求:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"path": "/path/to/video.mp4"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "5dea6618a606e7c7",
|
||||||
|
"file_name": "video.mp4",
|
||||||
|
"duration": 596.458333,
|
||||||
|
"width": 320,
|
||||||
|
"height": 180,
|
||||||
|
"fps": 24.0,
|
||||||
|
"cached": true,
|
||||||
|
"format": {...},
|
||||||
|
"streams": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/v1/videos
|
||||||
|
**列出所有已註冊影片**
|
||||||
|
```bash
|
||||||
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"videos": [
|
||||||
|
{
|
||||||
|
"uuid": "a03485a40b2df2d3",
|
||||||
|
"file_path": "/path/to/video.mp4",
|
||||||
|
"file_name": "video.mp4",
|
||||||
|
"duration": 596.458333,
|
||||||
|
"width": 320,
|
||||||
|
"height": 180
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/v1/lookup
|
||||||
|
**查詢影片資訊**
|
||||||
|
```bash
|
||||||
|
# 依 UUID 查詢
|
||||||
|
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?uuid=a03485a40b2df2d3"
|
||||||
|
|
||||||
|
# 依路徑查詢
|
||||||
|
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4"
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "a03485a40b2df2d3",
|
||||||
|
"file_path": "/path/to/video.mp4",
|
||||||
|
"file_name": "video.mp4",
|
||||||
|
"duration": 596.458333
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 搜尋功能
|
||||||
|
|
||||||
|
#### POST /api/v1/search
|
||||||
|
**語意搜尋(標準格式)**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/v1/search \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-d '{"query": "search term", "limit": 5}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**請求:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "search term",
|
||||||
|
"limit": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"uuid": "a1b10138a6bbb0cd",
|
||||||
|
"chunk_id": "sentence_0001",
|
||||||
|
"chunk_type": "sentence",
|
||||||
|
"start_time": 10.5,
|
||||||
|
"end_time": 15.2,
|
||||||
|
"text": "Found text matching query",
|
||||||
|
"score": 0.85
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"query": "search term"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/v1/n8n/search
|
||||||
|
**語意搜尋(n8n 格式)**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-d '{"query": "search term", "limit": 5}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "search term",
|
||||||
|
"count": 1,
|
||||||
|
"hits": [
|
||||||
|
{
|
||||||
|
"id": "sentence_0001",
|
||||||
|
"vid": "a1b10138a6bbb0cd",
|
||||||
|
"start": 10.5,
|
||||||
|
"end": 15.2,
|
||||||
|
"title": "Chunk sentence_0001",
|
||||||
|
"text": "Found text matching query",
|
||||||
|
"score": 0.85,
|
||||||
|
"file_path": "/path/to/video.mp4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/v1/search/hybrid
|
||||||
|
**混合搜尋(向量 + 關鍵字)**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/v1/search/hybrid \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-d '{"query": "search term", "limit": 5}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**請求:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "search term",
|
||||||
|
"limit": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應:** 與 `/api/v1/search` 相同格式
|
||||||
|
|
||||||
|
### 4. 任務管理
|
||||||
|
|
||||||
|
#### GET /api/v1/jobs
|
||||||
|
**列出所有處理任務**
|
||||||
|
```bash
|
||||||
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/jobs
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jobs": [
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"uuid": "a03485a40b2df2d3",
|
||||||
|
"status": "running",
|
||||||
|
"current_processor": null,
|
||||||
|
"progress_current": 0,
|
||||||
|
"progress_total": 0,
|
||||||
|
"created_at": "2026-03-26 13:39:37.830465",
|
||||||
|
"started_at": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/v1/jobs/:uuid
|
||||||
|
**取得特定任務詳情**
|
||||||
|
```bash
|
||||||
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/jobs/a03485a40b2df2d3
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"uuid": "a03485a40b2df2d3",
|
||||||
|
"status": "running",
|
||||||
|
"current_processor": null,
|
||||||
|
"progress_current": 0,
|
||||||
|
"progress_total": 0,
|
||||||
|
"processors": [
|
||||||
|
{
|
||||||
|
"processor_type": "asr",
|
||||||
|
"status": "completed",
|
||||||
|
"started_at": "2026-03-26 05:39:40.275468",
|
||||||
|
"completed_at": "2026-03-26 07:19:43.166613",
|
||||||
|
"duration_secs": 6002.891145,
|
||||||
|
"error_message": null
|
||||||
|
},
|
||||||
|
// ... 其他處理器
|
||||||
|
],
|
||||||
|
"created_at": "2026-03-26 13:39:37.830465",
|
||||||
|
"started_at": null,
|
||||||
|
"updated_at": "2026-03-26 07:19:16.338406"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 系統管理
|
||||||
|
|
||||||
|
#### GET /api/v1/progress/:uuid
|
||||||
|
**取得影片處理進度**
|
||||||
|
```bash
|
||||||
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/progress/a03485a40b2df2d3
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "a03485a40b2df2d3",
|
||||||
|
"user": null,
|
||||||
|
"group": null,
|
||||||
|
"file_name": "video.mp4",
|
||||||
|
"duration": 596.458333,
|
||||||
|
"overall_progress": 0,
|
||||||
|
"cpu_percent": 0.2,
|
||||||
|
"gpu_percent": null,
|
||||||
|
"memory_percent": 0.1,
|
||||||
|
"memory_mb": 16720,
|
||||||
|
"processors": [
|
||||||
|
{
|
||||||
|
"name": "asr",
|
||||||
|
"status": "pending",
|
||||||
|
"current": 0,
|
||||||
|
"total": 0,
|
||||||
|
"progress": 0,
|
||||||
|
"message": ""
|
||||||
|
},
|
||||||
|
// ... 其他處理器
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/v1/config/cache
|
||||||
|
**切換快取功能**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3002/api/v1/config/cache \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-d '{"enabled": true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**請求:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"cache_enabled": true,
|
||||||
|
"message": "Cache enabled"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速工作流程
|
||||||
|
|
||||||
|
### 1. 註冊並處理影片
|
||||||
|
```bash
|
||||||
|
# 1. 註冊影片
|
||||||
|
curl -X POST http://localhost:3002/api/v1/register \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-d '{"path": "/path/to/video.mp4"}'
|
||||||
|
|
||||||
|
# 回應包含 UUID: 5dea6618a606e7c7
|
||||||
|
|
||||||
|
# 2. 監控進度
|
||||||
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/progress/5dea6618a606e7c7
|
||||||
|
|
||||||
|
# 3. 查看任務狀態
|
||||||
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/jobs/5dea6618a606e7c7
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 搜尋影片內容
|
||||||
|
```bash
|
||||||
|
# 1. 列出所有影片
|
||||||
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||||
|
|
||||||
|
# 2. 搜尋內容
|
||||||
|
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-d '{"query": "charade scene", "limit": 10}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 系統管理
|
||||||
|
```bash
|
||||||
|
# 1. 檢查系統健康
|
||||||
|
curl http://localhost:3002/health/detailed
|
||||||
|
|
||||||
|
# 2. 管理快取
|
||||||
|
curl -X POST http://localhost:3002/api/v1/config/cache \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-d '{"enabled": false}'
|
||||||
|
|
||||||
|
# 3. 刪除影片(需要時)
|
||||||
|
curl -X POST http://localhost:3002/api/v1/unregister \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-d '{"uuid": "5dea6618a606e7c7"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 注意事項
|
||||||
|
|
||||||
|
1. **API Key 格式:**
|
||||||
|
- 使用完整 API Key:`muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69`
|
||||||
|
- 系統存儲的是 SHA256 哈希值
|
||||||
|
|
||||||
|
2. **路徑格式:**
|
||||||
|
- 絕對路徑:`/Users/accusys/test_video/video.mp4`
|
||||||
|
- 相對路徑:`./demo/video.mp4`(相對於 SFTPGo 資料目錄)
|
||||||
|
|
||||||
|
3. **回應時間:**
|
||||||
|
- 健康檢查:< 100ms
|
||||||
|
- 搜尋:取決於查詢複雜度,通常 100-500ms
|
||||||
|
- 影片註冊:立即返回,背景處理可能需要數分鐘到數小時
|
||||||
|
|
||||||
|
4. **錯誤處理:**
|
||||||
|
- 401: 認證失敗
|
||||||
|
- 404: 資源不存在
|
||||||
|
- 500: 伺服器內部錯誤
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 相關文件
|
||||||
|
|
||||||
|
- [API 參考指南](./API_REFERENCE.md) - 詳細 API 說明
|
||||||
|
- [API 範例總覽](./API_EXAMPLES.md) - 完整使用範例
|
||||||
|
- [API 端點列表](./API_ENDPOINTS.md) - 端點簡介
|
||||||
|
- [Curl 範例指南](./API_CURL_EXAMPLES.md) - curl 命令範例
|
||||||
|
- [n8n 整合指南](./API_N8N_GUIDE.md) - n8n 工作流程整合
|
||||||
+90
-9
@@ -4,7 +4,7 @@
|
|||||||
|------|------|
|
|------|------|
|
||||||
| 建立者 | Warren |
|
| 建立者 | Warren |
|
||||||
| 建立時間 | 2026-03-18 |
|
| 建立時間 | 2026-03-18 |
|
||||||
| 文件版本 | V1.0 |
|
| 文件版本 | V1.3 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,8 @@
|
|||||||
|------|------|------|--------|-----------|
|
|------|------|------|--------|-----------|
|
||||||
| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||||
| V1.1 | 2026-03-23 | 更新端點與實際一致 | OpenCode | - |
|
| V1.1 | 2026-03-23 | 更新端點與實際一致 | OpenCode | - |
|
||||||
|
| V1.2 | 2026-03-25 | 新增快取/刪除 API | OpenCode | - |
|
||||||
|
| V1.3 | 2026-03-26 | 修正認證聲明與API回應格式 | OpenCode | - |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -37,7 +39,22 @@
|
|||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
Currently no authentication is required.
|
**API Key 認證:**
|
||||||
|
|
||||||
|
所有 `/api/v1/*` 端點需要 `X-API-Key` header 進行認證。
|
||||||
|
|
||||||
|
**公開端點:**
|
||||||
|
- `GET /health` - 健康檢查
|
||||||
|
- `GET /health/detailed` - 詳細健康檢查
|
||||||
|
|
||||||
|
**認證格式:**
|
||||||
|
```bash
|
||||||
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||||
|
```
|
||||||
|
|
||||||
|
**API Key 管理:**
|
||||||
|
- 使用 `/api/v1/api-keys` 端點管理 API Keys
|
||||||
|
- 詳細說明請參考 [API Key Management Guide](../docs/API_KEY_MANAGEMENT.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -64,10 +81,12 @@ Register a video file to the system.
|
|||||||
{
|
{
|
||||||
"uuid": "5dea6618a606e7c7",
|
"uuid": "5dea6618a606e7c7",
|
||||||
"video_id": 1,
|
"video_id": 1,
|
||||||
|
"job_id": 10,
|
||||||
"file_name": "video.mp4",
|
"file_name": "video.mp4",
|
||||||
"duration": 120.5,
|
"duration": 120.5,
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080,
|
||||||
|
"already_exists": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -75,6 +94,7 @@ Register a video file to the system.
|
|||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3002/api/v1/register \
|
curl -X POST http://localhost:3002/api/v1/register \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"path": "/Users/accusys/test_video/BigBuckBunny_320x180.mp4"}'
|
-d '{"path": "/Users/accusys/test_video/BigBuckBunny_320x180.mp4"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -151,7 +171,7 @@ Get real-time processing progress via Redis.
|
|||||||
**Example:**
|
**Example:**
|
||||||
```bash
|
```bash
|
||||||
# Get progress for specific video
|
# Get progress for specific video
|
||||||
curl http://localhost:3002/api/v1/progress/5dea6618a606e7c7
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/progress/5dea6618a606e7c7
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -198,6 +218,7 @@ Search video chunks using natural language queries (RAG).
|
|||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3002/api/v1/search \
|
curl -X POST http://localhost:3002/api/v1/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"query": "machine learning", "limit": 5}'
|
-d '{"query": "machine learning", "limit": 5}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -237,7 +258,7 @@ N8n-compatible search endpoint with standardized response format for direct work
|
|||||||
"title": "Sunset Scene",
|
"title": "Sunset Scene",
|
||||||
"text": "The sun slowly sets over the ocean...",
|
"text": "The sun slowly sets over the ocean...",
|
||||||
"score": 0.92,
|
"score": 0.92,
|
||||||
"media_url": "https://wp.momentry.ddns.net/video.mp4"
|
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -254,12 +275,13 @@ N8n-compatible search endpoint with standardized response format for direct work
|
|||||||
| `hits[].title` | string | Chunk title (from metadata or auto-generated) |
|
| `hits[].title` | string | Chunk title (from metadata or auto-generated) |
|
||||||
| `hits[].text` | string | Text content |
|
| `hits[].text` | string | Text content |
|
||||||
| `hits[].score` | number | Relevance score (0-1) |
|
| `hits[].score` | number | Relevance score (0-1) |
|
||||||
| `hits[].media_url` | string | Full media URL (optional) |
|
| `hits[].file_path` | string | Full file path to video file |
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"query": "sunset", "limit": 5}'
|
-d '{"query": "sunset", "limit": 5}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -295,10 +317,10 @@ Lookup video UUID by path or get video details by UUID.
|
|||||||
**Example:**
|
**Example:**
|
||||||
```bash
|
```bash
|
||||||
# Lookup by path
|
# Lookup by path
|
||||||
curl "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4"
|
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4"
|
||||||
|
|
||||||
# Lookup by UUID
|
# Lookup by UUID
|
||||||
curl "http://localhost:3002/api/v1/lookup?uuid=5dea6618a606e7c7"
|
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?uuid=5dea6618a606e7c7"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -326,7 +348,7 @@ List all registered videos.
|
|||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:3002/api/v1/videos
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -384,6 +406,63 @@ curl http://localhost:3002/api/v1/videos
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Cache Toggle
|
||||||
|
|
||||||
|
Toggle caching at runtime.
|
||||||
|
|
||||||
|
**Endpoint:** `POST /api/v1/config/cache`
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `enabled` | boolean | Yes | Enable (true) or disable (false) cache |
|
||||||
|
|
||||||
|
**Response (200):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cache_enabled": true,
|
||||||
|
"message": "Cache toggled successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Unregister Video
|
||||||
|
|
||||||
|
Delete a video and all associated data (chunks, processor results, thumbnails).
|
||||||
|
|
||||||
|
**Endpoint:** `POST /api/v1/unregister`
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "5dea6618a606e7c7"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `uuid` | string | Yes | Video UUID (16 character hex) |
|
||||||
|
|
||||||
|
**Response (200):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Video unregistered successfully",
|
||||||
|
"uuid": "5dea6618a606e7c7"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Warning:** This operation is irreversible and will delete all associated chunks, processor results, and thumbnails.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
**400 Bad Request**
|
**400 Bad Request**
|
||||||
@@ -445,3 +524,5 @@ cargo run --bin momentry -- server --host 127.0.0.1 --port 3002
|
|||||||
| Search | `POST /api/v1/search` |
|
| Search | `POST /api/v1/search` |
|
||||||
| List videos | `GET /api/v1/videos` |
|
| List videos | `GET /api/v1/videos` |
|
||||||
| Lookup | `GET /api/v1/lookup?uuid=<uuid>` |
|
| Lookup | `GET /api/v1/lookup?uuid=<uuid>` |
|
||||||
|
| Toggle cache | `POST /api/v1/config/cache` |
|
||||||
|
| Delete video | `POST /api/v1/unregister` |
|
||||||
|
|||||||
@@ -0,0 +1,388 @@
|
|||||||
|
# Momentry Core API 教育訓練手冊
|
||||||
|
|
||||||
|
> **對象**: marcom 團隊
|
||||||
|
> **版本**: V1.4 | **日期**: 2026-03-25
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 快速開始
|
||||||
|
|
||||||
|
### 基本資訊
|
||||||
|
|
||||||
|
| 項目 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| API 網址 | `https://api.momentry.ddns.net` |
|
||||||
|
| 認證方式 | Header `X-API-Key` |
|
||||||
|
| 格式 | JSON |
|
||||||
|
|
||||||
|
### Demo 測試帳號
|
||||||
|
|
||||||
|
#### API Key(用於 API 認證)
|
||||||
|
|
||||||
|
```
|
||||||
|
X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SFTPGo(用於影片上傳)
|
||||||
|
|
||||||
|
| 項目 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| SFTP 主機 | `sftpgo.momentry.ddns.net` |
|
||||||
|
| SFTP 連接埠 | `2022` |
|
||||||
|
| 用戶名 | `demo` |
|
||||||
|
| 密碼 | `demopassword123` |
|
||||||
|
| Web 管理介面 | `https://sftpgo.momentry.ddns.net` |
|
||||||
|
|
||||||
|
**使用方式**:透過 SFTP 上傳影片,系統會自動註冊並處理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 快速範例
|
||||||
|
|
||||||
|
### 查詢所有影片
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
|
||||||
|
"https://api.momentry.ddns.net/api/v1/videos"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查詢單一影片
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
|
||||||
|
"https://api.momentry.ddns.net/api/v1/videos/{uuid}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查詢處理任務狀態
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
|
||||||
|
"https://api.momentry.ddns.net/api/v1/jobs/{uuid}"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. API 端點說明
|
||||||
|
|
||||||
|
### 3.1 影片相關
|
||||||
|
|
||||||
|
#### GET /api/v1/videos
|
||||||
|
取得所有影片列表
|
||||||
|
|
||||||
|
**回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"videos": [
|
||||||
|
{
|
||||||
|
"uuid": "5dea6618a606e7c7",
|
||||||
|
"filename": "demo_video.mp4",
|
||||||
|
"duration": 123.45,
|
||||||
|
"status": "ready",
|
||||||
|
"created_at": "2026-03-25T10:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/v1/videos/:uuid
|
||||||
|
取得單一影片詳情
|
||||||
|
|
||||||
|
### 3.2 搜尋與分段查詢
|
||||||
|
|
||||||
|
#### POST /api/v1/search
|
||||||
|
向量搜尋,查詢分段(Chunk)詳情
|
||||||
|
|
||||||
|
**請求參數**:
|
||||||
|
| 參數 | 類型 | 必填 | 說明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `query` | string | 是 | 搜尋關鍵字 |
|
||||||
|
| `limit` | number | 否 | 回傳數量(預設 10) |
|
||||||
|
| `uuid` | string | 否 | 只搜尋指定影片 |
|
||||||
|
|
||||||
|
**請求範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "天氣",
|
||||||
|
"limit": 10,
|
||||||
|
"uuid": "5dea6618a606e7c7"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"uuid": "39567a0eb16f39fd",
|
||||||
|
"chunk_id": "sentence_1471",
|
||||||
|
"chunk_type": "sentence",
|
||||||
|
"start_time": 5309.08,
|
||||||
|
"end_time": 5311.08,
|
||||||
|
"text": "influenced by a vital way,",
|
||||||
|
"score": 0.68
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"query": "天氣"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Chunk 欄位說明**:
|
||||||
|
| 欄位 | 說明 | 範例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `uuid` | 影片唯一識別碼 | `39567a0eb16f39fd` |
|
||||||
|
| `chunk_id` | 分段識別碼 | `sentence_1471` |
|
||||||
|
| `chunk_type` | 分段類型 | `sentence` / `cut` / `time` / `trace` / `story` |
|
||||||
|
| `start_time` | 開始時間(秒) | `5309.08` |
|
||||||
|
| `end_time` | 結束時間(秒) | `5311.08` |
|
||||||
|
| `text` | 內容文字 | `influenced by a vital way` |
|
||||||
|
| `score` | 相似度分數(0-1) | `0.68` |
|
||||||
|
|
||||||
|
**Chunk 類型說明**:
|
||||||
|
| 類型 | 說明 | 來源 |
|
||||||
|
|------|------|------|
|
||||||
|
| `sentence` | 語音轉文字片段 | ASR 處理 |
|
||||||
|
| `cut` | 場景變化片段 | CUT 處理 |
|
||||||
|
| `time` | 固定時間分段 | 系統自動切割 |
|
||||||
|
| `trace` | 軌跡追蹤片段 | YOLO 追蹤 |
|
||||||
|
| `story` | 故事線片段(父子關係) | Story 分析 |
|
||||||
|
|
||||||
|
#### POST /api/v1/n8n/search
|
||||||
|
n8n 專用搜尋(包含完整影片檔案路徑 file_path)
|
||||||
|
|
||||||
|
**請求參數**: 與 `/search` 相同
|
||||||
|
|
||||||
|
**回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "天氣",
|
||||||
|
"count": 2,
|
||||||
|
"hits": [
|
||||||
|
{
|
||||||
|
"id": "sentence_1471",
|
||||||
|
"vid": "39567a0eb16f39fd",
|
||||||
|
"start": 5309.08,
|
||||||
|
"end": 5311.08,
|
||||||
|
"title": "Chunk sentence_1471",
|
||||||
|
"text": "influenced by a vital way,",
|
||||||
|
"score": 0.68,
|
||||||
|
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**與 /search 的差異**:
|
||||||
|
| 欄位 | `/search` | `/n8n/search` |
|
||||||
|
|------|-----------|----------------|
|
||||||
|
| 影片 UUID | `uuid` | `vid` |
|
||||||
|
| Chunk ID | `chunk_id` | `id` |
|
||||||
|
| 開始時間 | `start_time` | `start` |
|
||||||
|
| 結束時間 | `end_time` | `end` |
|
||||||
|
| 相似度分數 | `score` | `score` |
|
||||||
|
| **檔案路徑** | ❌ | ✅ `file_path` |
|
||||||
|
|
||||||
|
> **注意**: `file_path` 是影片的實際路徑,可用於本地播放。
|
||||||
|
|
||||||
|
### 3.3 任務相關
|
||||||
|
|
||||||
|
### 3.4 任務相關
|
||||||
|
|
||||||
|
#### GET /api/v1/jobs/:uuid
|
||||||
|
查詢處理任務狀態
|
||||||
|
|
||||||
|
**回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "9760d0820f0cf9a7",
|
||||||
|
"video_uuid": "5dea6618a606e7c7",
|
||||||
|
"status": "completed",
|
||||||
|
"progress": 100,
|
||||||
|
"created_at": "2026-03-25T10:00:00Z",
|
||||||
|
"completed_at": "2026-03-25T10:05:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/v1/jobs
|
||||||
|
查詢所有任務
|
||||||
|
|
||||||
|
**查詢參數**:
|
||||||
|
| 參數 | 說明 | 範例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `status` | 篩選狀態 | `pending`, `processing`, `completed`, `failed` |
|
||||||
|
| `limit` | 回傳數量 | `10` |
|
||||||
|
|
||||||
|
**範例**:
|
||||||
|
```bash
|
||||||
|
curl -s -H "X-API-Key: ..." \
|
||||||
|
"https://api.momentry.ddns.net/api/v1/jobs?status=completed&limit=5"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 系統管理
|
||||||
|
|
||||||
|
#### POST /api/v1/config/cache
|
||||||
|
切換快取功能(管理員專用)
|
||||||
|
|
||||||
|
**請求範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cache_enabled": true,
|
||||||
|
"message": "Cache toggled successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/v1/unregister
|
||||||
|
刪除影片及其所有關聯資料(管理員專用)
|
||||||
|
|
||||||
|
**請求範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "5dea6618a606e7c7"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Video unregistered successfully",
|
||||||
|
"uuid": "5dea6618a606e7c7"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**: 此操作會刪除影片及其所有分段、處理結果、縮圖等關聯資料,**無法復原**。
|
||||||
|
|
||||||
|
### 3.6 健康檢查
|
||||||
|
|
||||||
|
#### GET /health
|
||||||
|
服務健康狀態(**無需認證**)
|
||||||
|
|
||||||
|
**回應**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"version": "0.9.20260325_144654"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. n8n Workflow 範例
|
||||||
|
|
||||||
|
### 4.1 基本設定
|
||||||
|
|
||||||
|
在 n8n workflow 中使用 HTTP Request 節點:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ HTTP Request │
|
||||||
|
├─────────────────┤
|
||||||
|
│ Method: GET │
|
||||||
|
│ URL: https://api.momentry.ddns.net/api/v1/videos
|
||||||
|
│ Headers: │
|
||||||
|
│ X-API-Key: │
|
||||||
|
│ [YOUR_KEY] │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ 處理回應資料 │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 範例:檢查任務狀態
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// n8n Function Node 範例
|
||||||
|
const jobUuid = $input.item.json.uuid;
|
||||||
|
|
||||||
|
return [{
|
||||||
|
json: {
|
||||||
|
method: "GET",
|
||||||
|
url: `https://api.momentry.ddns.net/api/v1/jobs/${jobUuid}`,
|
||||||
|
headers: {
|
||||||
|
"X-API-Key": "YOUR_API_KEY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 常見問題
|
||||||
|
|
||||||
|
### Q: 返回 401 錯誤怎麼辦?
|
||||||
|
確認 Header 中有正確的 `X-API-Key` 值
|
||||||
|
|
||||||
|
### Q: 如何確認影片處理完成?
|
||||||
|
```
|
||||||
|
GET /api/v1/jobs/{uuid}
|
||||||
|
```
|
||||||
|
檢查 `status` 是否為 `completed`
|
||||||
|
|
||||||
|
### Q: 查不到資料?
|
||||||
|
確認 UUID 格式正確(16碼 hex 字串)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 快速參考卡
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Momentry API 速查 │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 查詢所有影片 GET /api/v1/videos │
|
||||||
|
│ 查詢單一影片 GET /api/v1/videos/:uuid │
|
||||||
|
│ 向量搜尋 POST /api/v1/search │
|
||||||
|
│ n8n 搜尋 POST /api/v1/n8n/search │
|
||||||
|
│ 查詢任務狀態 GET /api/v1/jobs/:uuid │
|
||||||
|
│ 查詢所有任務 GET /api/v1/jobs │
|
||||||
|
│ 快取設定 POST /api/v1/config/cache (管理員) │
|
||||||
|
│ 刪除影片 POST /api/v1/unregister (管理員) │
|
||||||
|
│ 健康檢查 GET /health (免認證) │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Header: X-API-Key: [YOUR_KEY] │
|
||||||
|
│ URL: https://api.momentry.ddns.net │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附錄:回應狀態說明
|
||||||
|
|
||||||
|
### 任務狀態 (status)
|
||||||
|
|
||||||
|
| 狀態 | 說明 |
|
||||||
|
|------|------|
|
||||||
|
| `pending` | 等待處理 |
|
||||||
|
| `processing` | 處理中 |
|
||||||
|
| `completed` | 已完成 |
|
||||||
|
| `failed` | 處理失敗 |
|
||||||
|
|
||||||
|
### 影片狀態 (status)
|
||||||
|
|
||||||
|
| 狀態 | 說明 |
|
||||||
|
|------|------|
|
||||||
|
| `uploading` | 上傳中 |
|
||||||
|
| `pending` | 等待處理 |
|
||||||
|
| `processing` | 處理中 |
|
||||||
|
| `ready` | 已就緒 |
|
||||||
|
| `error` | 錯誤 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附錄:版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 內容 | 操作人 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| V1.0 | 2026-03-25 | 初版建立 | OpenCode |
|
||||||
|
| V1.1 | 2026-03-25 | 新增快取/刪除 API、搜尋端點文件 | OpenCode |
|
||||||
|
| V1.2 | 2026-03-25 | 新增 Chunk 欄位說明、類型、播放方式 | OpenCode |
|
||||||
|
| V1.3 | 2026-03-25 | 新增 Demo 測試帳號(SFTPGo)| OpenCode |
|
||||||
|
| V1.4 | 2026-03-25 | 更新 n8n 搜尋回傳欄位說明 (media_url→file_path) | OpenCode |
|
||||||
@@ -2,12 +2,21 @@
|
|||||||
|
|
||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 版本 | V1.0 |
|
| 版本 | V1.1 |
|
||||||
| 日期 | 2026-03-23 |
|
| 日期 | 2026-03-25 |
|
||||||
| 用途 | 在 WordPress 中呼叫 Momentry API |
|
| 用途 | 在 WordPress 中呼叫 Momentry API |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.1 | 2026-03-25 | 更新: n8n 搜尋回傳 `file_path` 取代 `media_url`,新增 API Key 驗證說明 | OpenCode | deepseek-reasoner |
|
||||||
|
| V1.0 | 2026-03-23 | 創建 WordPress API 指南 | Warren | OpenCode / MiniMax M2.5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## API URL
|
## API URL
|
||||||
|
|
||||||
在 WordPress 中呼叫 API,**請使用外部 URL**:
|
在 WordPress 中呼叫 API,**請使用外部 URL**:
|
||||||
@@ -20,6 +29,20 @@ https://api.momentry.ddns.net
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## API 認證
|
||||||
|
|
||||||
|
所有 `/api/v1/*` 端點(除了健康檢查)都需要 API Key 認證。請在請求標頭中加入:
|
||||||
|
|
||||||
|
```
|
||||||
|
'headers' => ['Content-Type' => 'application/json', 'X-API-Key' => 'YOUR_API_KEY']
|
||||||
|
```
|
||||||
|
|
||||||
|
**目前示範使用的 API Key**: `demo_api_key_12345`
|
||||||
|
|
||||||
|
> **注意**: 正式環境請使用安全的 API Key 管理機制,避免在客戶端 JavaScript 中暴露 API Key。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 常用端點
|
## 常用端點
|
||||||
|
|
||||||
| 方法 | 端點 | 說明 |
|
| 方法 | 端點 | 說明 |
|
||||||
@@ -45,7 +68,7 @@ $data = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
$response = wp_remote_post($api_url, [
|
$response = wp_remote_post($api_url, [
|
||||||
'headers' => ['Content-Type' => 'application/json'],
|
'headers' => ['Content-Type' => 'application/json', 'X-API-Key' => 'YOUR_API_KEY'],
|
||||||
'body' => json_encode($data),
|
'body' => json_encode($data),
|
||||||
'timeout' => 30
|
'timeout' => 30
|
||||||
]);
|
]);
|
||||||
@@ -65,7 +88,10 @@ if (is_wp_error($response)) {
|
|||||||
<?php
|
<?php
|
||||||
$api_url = 'https://api.momentry.ddns.net/api/v1/videos';
|
$api_url = 'https://api.momentry.ddns.net/api/v1/videos';
|
||||||
|
|
||||||
$response = wp_remote_get($api_url, ['timeout' => 30]);
|
$response = wp_remote_get($api_url, [
|
||||||
|
'headers' => ['X-API-Key' => 'YOUR_API_KEY'],
|
||||||
|
'timeout' => 30
|
||||||
|
]);
|
||||||
|
|
||||||
if (!is_wp_error($response)) {
|
if (!is_wp_error($response)) {
|
||||||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
$body = json_decode(wp_remote_retrieve_body($response), true);
|
||||||
@@ -83,7 +109,10 @@ if (!is_wp_error($response)) {
|
|||||||
$uuid = '5dea6618a606e7c7';
|
$uuid = '5dea6618a606e7c7';
|
||||||
$api_url = 'https://api.momentry.ddns.net/api/v1/lookup?uuid=' . $uuid;
|
$api_url = 'https://api.momentry.ddns.net/api/v1/lookup?uuid=' . $uuid;
|
||||||
|
|
||||||
$response = wp_remote_get($api_url, ['timeout' => 30]);
|
$response = wp_remote_get($api_url, [
|
||||||
|
'headers' => ['X-API-Key' => 'YOUR_API_KEY'],
|
||||||
|
'timeout' => 30
|
||||||
|
]);
|
||||||
|
|
||||||
if (!is_wp_error($response)) {
|
if (!is_wp_error($response)) {
|
||||||
$video = json_decode(wp_remote_retrieve_body($response), true);
|
$video = json_decode(wp_remote_retrieve_body($response), true);
|
||||||
@@ -104,7 +133,7 @@ if (!is_wp_error($response)) {
|
|||||||
async function searchVideos(query, limit = 10) {
|
async function searchVideos(query, limit = 10) {
|
||||||
const response = await fetch('https://api.momentry.ddns.net/api/v1/n8n/search', {
|
const response = await fetch('https://api.momentry.ddns.net/api/v1/n8n/search', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', 'X-API-Key': 'YOUR_API_KEY' },
|
||||||
body: JSON.stringify({ query, limit })
|
body: JSON.stringify({ query, limit })
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,6 +161,24 @@ searchVideos('charade', 5)
|
|||||||
|
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php
|
||||||
|
// 將文件路徑轉換為可訪問的 URL
|
||||||
|
function convert_file_path_to_url($file_path) {
|
||||||
|
// 範例: 將 SFTPGo 文件路徑轉換為 web URL
|
||||||
|
// /Users/accusys/momentry/var/sftpgo/data/demo/video.mp4
|
||||||
|
// → https://sftpgo.example.com/demo/video.mp4
|
||||||
|
|
||||||
|
// 移除基本路徑
|
||||||
|
$base_path = '/Users/accusys/momentry/var/sftpgo/data/';
|
||||||
|
if (strpos($file_path, $base_path) === 0) {
|
||||||
|
$relative_path = substr($file_path, strlen($base_path));
|
||||||
|
// 替換為實際的 SFTPGo web URL
|
||||||
|
return 'https://sftpgo.example.com/' . $relative_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果無法轉換,返回原始路徑
|
||||||
|
return $file_path;
|
||||||
|
}
|
||||||
|
|
||||||
// 註冊短碼
|
// 註冊短碼
|
||||||
add_shortcode('momentry_search', function($atts) {
|
add_shortcode('momentry_search', function($atts) {
|
||||||
$atts = shortcode_atts([
|
$atts = shortcode_atts([
|
||||||
@@ -144,7 +191,10 @@ add_shortcode('momentry_search', function($atts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$response = wp_remote_post('https://api.momentry.ddns.net/api/v1/n8n/search', [
|
$response = wp_remote_post('https://api.momentry.ddns.net/api/v1/n8n/search', [
|
||||||
'headers' => ['Content-Type' => 'application/json'],
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'X-API-Key' => 'YOUR_API_KEY' // 替換為實際的 API Key
|
||||||
|
],
|
||||||
'body' => json_encode([
|
'body' => json_encode([
|
||||||
'query' => $atts['query'],
|
'query' => $atts['query'],
|
||||||
'limit' => (int)$atts['limit']
|
'limit' => (int)$atts['limit']
|
||||||
@@ -164,10 +214,15 @@ add_shortcode('momentry_search', function($atts) {
|
|||||||
|
|
||||||
$output = '<ul class="momentry-results">';
|
$output = '<ul class="momentry-results">';
|
||||||
foreach ($data['hits'] as $hit) {
|
foreach ($data['hits'] as $hit) {
|
||||||
|
// 注意: API 現在返回 file_path 而非 media_url
|
||||||
|
// 需要將文件路徑轉換為可訪問的 URL
|
||||||
|
$file_path = $hit['file_path'];
|
||||||
|
$video_url = convert_file_path_to_url($file_path); // 需要實作此函數
|
||||||
|
|
||||||
$output .= sprintf(
|
$output .= sprintf(
|
||||||
'<li>%s <a href="%s?start=%s">播放</a></li>',
|
'<li>%s <a href="%s?start=%s">播放</a></li>',
|
||||||
esc_html($hit['text']),
|
esc_html($hit['text']),
|
||||||
$hit['media_url'],
|
$video_url,
|
||||||
$hit['start']
|
$hit['start']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -199,7 +254,7 @@ add_action('rest_api_init', function() {
|
|||||||
$response = wp_remote_post(
|
$response = wp_remote_post(
|
||||||
'https://api.momentry.ddns.net/api/v1/n8n/search',
|
'https://api.momentry.ddns.net/api/v1/n8n/search',
|
||||||
[
|
[
|
||||||
'headers' => ['Content-Type' => 'application/json'],
|
'headers' => ['Content-Type' => 'application/json', 'X-API-Key' => 'YOUR_API_KEY'],
|
||||||
'body' => json_encode([
|
'body' => json_encode([
|
||||||
'query' => $request->get_param('query'),
|
'query' => $request->get_param('query'),
|
||||||
'limit' => $request->get_param('limit', 10)
|
'limit' => $request->get_param('limit', 10)
|
||||||
|
|||||||
@@ -0,0 +1,461 @@
|
|||||||
|
# Momentry API 使用流程
|
||||||
|
|
||||||
|
> **目標**: 從影片上傳到搜尋的完整流程
|
||||||
|
> **適用**: WordPress / n8n 整合
|
||||||
|
> **版本**: V1.0 | **日期**: 2026-03-25
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 流程總覽
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
|
│ 1. 上傳 │ → │ 2. 註冊 │ → │ 3. 確認 │ → │ 4. 處理 │ → │ 5. 搜尋 │
|
||||||
|
│ SFTPGo │ │ 自動完成 │ │ UUID │ │ 查詢進度 │ │ 測試 │
|
||||||
|
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: 上傳影片
|
||||||
|
|
||||||
|
### 方式 A: SFTP 上傳(推薦)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 連線資訊
|
||||||
|
主機: sftpgo.momentry.ddns.net
|
||||||
|
連接埠: 2022
|
||||||
|
用戶名: demo
|
||||||
|
密碼: demopassword123
|
||||||
|
```
|
||||||
|
|
||||||
|
使用 FileZilla 或 SFTP 客戶端上傳到 `/` 目錄
|
||||||
|
|
||||||
|
### 方式 B: SFTP 命令列
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sshpass -p "demopassword123" sftp -P 2022 demo@sftpgo.momentry.ddns.net
|
||||||
|
```
|
||||||
|
|
||||||
|
上傳後確認檔案在 SFTPGo 中的位置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: 自動註冊
|
||||||
|
|
||||||
|
上傳後,系統會自動:
|
||||||
|
1. 偵測新檔案
|
||||||
|
2. 計算 UUID(SHA256)
|
||||||
|
3. 建立資料庫記錄
|
||||||
|
|
||||||
|
**無需手動操作**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: 確認註冊成功
|
||||||
|
|
||||||
|
### 查詢所有影片
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
"https://api.momentry.ddns.net/api/v1/videos" | jq '.videos | length'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查詢特定檔案
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
"https://api.momentry.ddns.net/api/v1/videos" | jq '.videos[] | select(.file_name | contains("你的檔案名"))'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 預期回應
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "952f5854b9febad1",
|
||||||
|
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/你的檔案.mp4",
|
||||||
|
"file_name": "你的檔案.mp4",
|
||||||
|
"duration": 123.45,
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**確認要點**:
|
||||||
|
- ✅ UUID 已產生(16位 hex)
|
||||||
|
- ✅ `file_path` 正確
|
||||||
|
- ✅ `duration` > 0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: 查詢處理進度
|
||||||
|
|
||||||
|
### 取得任務 UUID
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 從影片資訊取得 job_id
|
||||||
|
curl -s -H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
"https://api.momentry.ddns.net/api/v1/videos" | \
|
||||||
|
jq '.videos[] | select(.file_name == "你的檔案.mp4") | {uuid, job_id}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查詢任務狀態
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
"https://api.momentry.ddns.net/api/v1/jobs/{uuid}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 任務狀態說明
|
||||||
|
|
||||||
|
| status | 說明 | 動作 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `pending` | 等待處理 | 等待中 |
|
||||||
|
| `processing` | 處理中 | 繼續輪詢 |
|
||||||
|
| `completed` | 已完成 | 可進入 Step 5 |
|
||||||
|
| `failed` | 處理失敗 | 檢查錯誤 |
|
||||||
|
|
||||||
|
### n8n 輪詢範例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// n8n Workflow: 檢查處理狀態
|
||||||
|
const jobUuid = $input.item.json.job_uuid;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.momentry.ddns.net/api/v1/jobs/${jobUuid}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"X-API-Key": "YOUR_API_KEY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const job = await response.json();
|
||||||
|
|
||||||
|
// 狀態檢查
|
||||||
|
if (job.status === 'completed') {
|
||||||
|
return [{ json: { done: true, video_uuid: job.video_uuid } }];
|
||||||
|
} else {
|
||||||
|
return [{ json: { done: false, status: job.status } }];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: 搜尋測試
|
||||||
|
|
||||||
|
處理完成後,資料會入庫到向量資料庫,可進行搜尋測試。
|
||||||
|
|
||||||
|
### 測試向量搜尋
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "https://api.momentry.ddns.net/api/v1/search" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"query": "測試關鍵字",
|
||||||
|
"limit": 5
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 取得分段(Chunk)內容
|
||||||
|
|
||||||
|
搜尋結果會返回影片分段(Chunk),包含可播放的時間軸資訊:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"uuid": "39567a0eb16f39fd",
|
||||||
|
"chunk_id": "sentence_1471",
|
||||||
|
"chunk_type": "sentence",
|
||||||
|
"start_time": 5309.08,
|
||||||
|
"end_time": 5311.08,
|
||||||
|
"text": "influenced by a vital way,",
|
||||||
|
"score": 0.68
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Chunk 欄位說明**:
|
||||||
|
| 欄位 | 說明 |
|
||||||
|
|------|------|
|
||||||
|
| `uuid` | 影片 UUID(用於取得影片網址) |
|
||||||
|
| `chunk_id` | 分段 ID |
|
||||||
|
| `chunk_type` | 分段類型(sentence/cut/time/trace/story) |
|
||||||
|
| `start_time` | 開始時間(秒) |
|
||||||
|
| `end_time` | 結束時間(秒) |
|
||||||
|
| `text` | 語音內容文字 |
|
||||||
|
| `score` | 相似度分數(0-1) |
|
||||||
|
|
||||||
|
### 播放分段
|
||||||
|
|
||||||
|
取得 Chunk 後可組合成播放網址:
|
||||||
|
|
||||||
|
```
|
||||||
|
影片網址?start={start_time}&end={end_time}
|
||||||
|
```
|
||||||
|
|
||||||
|
範例:
|
||||||
|
```
|
||||||
|
https://wp.momentry.ddns.net/video.mp4?start=5309.08&end=5311.08
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完整 n8n Workflow 範例
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐
|
||||||
|
│ 觸發 (定時) │
|
||||||
|
└──────┬───────┘
|
||||||
|
▼
|
||||||
|
┌──────────────┐ ┌──────────────┐
|
||||||
|
│ 查詢影片 │────►│ 比對新檔案 │
|
||||||
|
│ /videos │ │ │
|
||||||
|
└──────┬───────┘ └──────────────┘
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌──────────────┐ ┌──────────────┐
|
||||||
|
│ 等待處理 │◄────│ 輪詢任務狀態 │
|
||||||
|
│ /jobs/:uuid │ │ /jobs/:uuid │
|
||||||
|
└──────┬───────┘ └──────────────┘
|
||||||
|
│
|
||||||
|
▼ (completed)
|
||||||
|
┌──────────────┐
|
||||||
|
│ 搜尋測試 │
|
||||||
|
│ /search │
|
||||||
|
└──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速參考
|
||||||
|
|
||||||
|
| 步驟 | API | 用途 |
|
||||||
|
|------|-----|------|
|
||||||
|
| 查詢影片 | `GET /api/v1/videos` | 確認上傳成功 |
|
||||||
|
| 查詢任務 | `GET /api/v1/jobs/:uuid` | 查看處理進度 |
|
||||||
|
| 搜尋內容 | `POST /api/v1/search` | 測試搜尋功能 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WordPress PHP 範例
|
||||||
|
|
||||||
|
### 基本設定
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
class Momentry_API {
|
||||||
|
private const API_URL = 'https://api.momentry.ddns.net';
|
||||||
|
private const API_KEY = 'YOUR_API_KEY';
|
||||||
|
|
||||||
|
public static function request(string $method, string $endpoint, ?array $data = null): array {
|
||||||
|
$url = self::API_URL . $endpoint;
|
||||||
|
|
||||||
|
$args = [
|
||||||
|
'method' => $method,
|
||||||
|
'headers' => [
|
||||||
|
'X-API-Key' => self::API_KEY,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
],
|
||||||
|
'timeout' => 30,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($data !== null) {
|
||||||
|
$args['body'] = json_encode($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = wp_remote_request($url, $args);
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
throw new Exception($response->get_error_message());
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_decode(wp_remote_retrieve_body($response), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getVideos(): array {
|
||||||
|
return self::request('GET', '/api/v1/videos');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getVideo(string $uuid): array {
|
||||||
|
return self::request('GET', "/api/v1/videos/{$uuid}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getJob(string $uuid): array {
|
||||||
|
return self::request('GET', "/api/v1/jobs/{$uuid}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function search(string $query, int $topK = 5): array {
|
||||||
|
return self::request('POST', '/api/v1/search', [
|
||||||
|
'query' => $query,
|
||||||
|
'top_k' => $topK,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: 確認註冊成功
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// 查詢所有影片
|
||||||
|
$videos = Momentry_API::getVideos();
|
||||||
|
|
||||||
|
foreach ($videos['videos'] as $video) {
|
||||||
|
echo "UUID: " . $video['uuid'] . "\n";
|
||||||
|
echo "檔案: " . $video['file_name'] . "\n";
|
||||||
|
echo "時長: " . $video['duration'] . " 秒\n";
|
||||||
|
echo "---\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查詢特定影片
|
||||||
|
$video = Momentry_API::getVideo('952f5854b9febad1');
|
||||||
|
print_r($video);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: 查詢處理進度
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// 取得任務狀態
|
||||||
|
$job = Momentry_API::getJob('9760d0820f0cf9a7');
|
||||||
|
|
||||||
|
switch ($job['status']) {
|
||||||
|
case 'pending':
|
||||||
|
echo "等待處理中...\n";
|
||||||
|
break;
|
||||||
|
case 'processing':
|
||||||
|
echo "處理中: " . $job['progress'] . "%\n";
|
||||||
|
break;
|
||||||
|
case 'completed':
|
||||||
|
echo "處理完成!\n";
|
||||||
|
break;
|
||||||
|
case 'failed':
|
||||||
|
echo "處理失敗: " . ($job['error'] ?? '未知錯誤') . "\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: 搜尋內容並取得 Chunk
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// 搜尋相關片段
|
||||||
|
$results = Momentry_API::search('測試關鍵字', 5);
|
||||||
|
|
||||||
|
foreach ($results['results'] as $result) {
|
||||||
|
echo "影片 UUID: " . $result['uuid'] . "\n";
|
||||||
|
echo "Chunk ID: " . $result['chunk_id'] . "\n";
|
||||||
|
echo "類型: " . $result['chunk_type'] . "\n";
|
||||||
|
echo "開始: " . $result['start_time'] . "s\n";
|
||||||
|
echo "結束: " . $result['end_time'] . "s\n";
|
||||||
|
echo "內容: " . ($result['text'] ?? '') . "\n";
|
||||||
|
echo "相似度: " . $result['score'] . "\n";
|
||||||
|
echo "---\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WordPress Shortcode 範例(可點擊播放)
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// 在 functions.php 中加入
|
||||||
|
add_shortcode('momentry_search', function($atts) {
|
||||||
|
$atts = shortcode_atts([
|
||||||
|
'query' => '',
|
||||||
|
'limit' => 10,
|
||||||
|
], $atts);
|
||||||
|
|
||||||
|
if (empty($atts['query'])) {
|
||||||
|
return '<p>請輸入搜尋關鍵字</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$results = Momentry_API::search($atts['query'], $atts['limit']);
|
||||||
|
|
||||||
|
if (empty($results['results'])) {
|
||||||
|
return '<p>找不到相關結果</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = '<div class="momentry-results">';
|
||||||
|
$html .= '<h3>搜尋結果: ' . esc_html($atts['query']) . '</h3>';
|
||||||
|
$html .= '<ul>';
|
||||||
|
|
||||||
|
foreach ($results['results'] as $result) {
|
||||||
|
$video_uuid = $result['uuid'];
|
||||||
|
$start = $result['start_time'] ?? 0;
|
||||||
|
$end = $result['end_time'] ?? 0;
|
||||||
|
$text = $result['text'] ?? '無文字描述';
|
||||||
|
|
||||||
|
$html .= '<li>';
|
||||||
|
$html .= '<a href="/player?uuid=' . esc_attr($video_uuid) .
|
||||||
|
'&start=' . esc_attr($start) .
|
||||||
|
'&end=' . esc_attr($end) . '">';
|
||||||
|
$html .= '播放 ' . $start . 's - ' . $end . 's';
|
||||||
|
$html .= '</a>';
|
||||||
|
$html .= '<br>';
|
||||||
|
$html .= '<small>相似度: ' . round($result['score'] * 100) . '%</small>';
|
||||||
|
$html .= '<br>';
|
||||||
|
$html .= esc_html($text);
|
||||||
|
$html .= '</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= '</ul></div>';
|
||||||
|
return $html;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return '<p>搜尋服務暫時無法使用</p>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用方式**:
|
||||||
|
```html
|
||||||
|
[momentry_search query="關鍵字" limit="5"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完整 n8n Workflow 範例
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐
|
||||||
|
│ 觸發 (定時) │
|
||||||
|
└──────┬───────┘
|
||||||
|
▼
|
||||||
|
┌──────────────┐ ┌──────────────┐
|
||||||
|
│ 查詢影片 │────►│ 比對新檔案 │
|
||||||
|
│ /videos │ │ │
|
||||||
|
└──────┬───────┘ └──────────────┘
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌──────────────┐ ┌──────────────┐
|
||||||
|
│ 等待處理 │◄────│ 輪詢任務狀態 │
|
||||||
|
│ /jobs/:uuid │ │ /jobs/:uuid │
|
||||||
|
└──────┬───────┘ └──────────────┘
|
||||||
|
│
|
||||||
|
▼ (completed)
|
||||||
|
┌──────────────┐
|
||||||
|
│ 搜尋測試 │
|
||||||
|
│ /search │
|
||||||
|
└──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**注意**:
|
||||||
|
- 處理時間視影片長度而定(1分鐘影片約需 2-5 分鐘處理)
|
||||||
|
- 大量影片時建議分批上傳
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附錄:版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 內容 | 操作人 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| V1.0 | 2026-03-25 | 初版建立 | OpenCode |
|
||||||
|
| V1.1 | 2026-03-25 | 新增 Chunk 取得與播放說明、Shortcode 範例 | OpenCode |
|
||||||
|
| V1.2 | 2026-03-25 | 修正 SFTPGo 主機名稱為 sftpgo.momentry.ddns.net | OpenCode |
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
# Momentry Chunk 資料結構說明
|
||||||
|
|
||||||
|
> **對象**: marcom 團隊
|
||||||
|
> **版本**: V1.0 | **日期**: 2026-03-25
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 什麼是 Chunk?
|
||||||
|
|
||||||
|
Chunk(片段)是影片處理後的最小單位。當影片上傳後,系統會自動:
|
||||||
|
|
||||||
|
1. **分析** - 偵測場景、人臉、姿態
|
||||||
|
2. **轉換** - 語音轉文字(ASR)
|
||||||
|
3. **分段** - 將內容切割成可搜尋的片段
|
||||||
|
4. **向量化** - 產生可搜尋的特徵向量
|
||||||
|
|
||||||
|
每個 Chunk 就是一個**可獨立搜尋的內容單位**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Chunk 資料結構
|
||||||
|
|
||||||
|
### 2.1 主要欄位
|
||||||
|
|
||||||
|
| 欄位名 | 類型 | 說明 | 範例 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `uuid` | 字串 (32) | 影片唯一識別碼 | `952f5854b9febad1` |
|
||||||
|
| `chunk_id` | 字串 (64) | Chunk 唯一識別碼 | `asr_00001` |
|
||||||
|
| `chunk_index` | 整數 | Chunk 順序號碼 | `1` |
|
||||||
|
| `chunk_type` | 字串 (32) | Chunk 類型 | `sentence` |
|
||||||
|
| `start_time` | 浮點數 | 開始時間(秒) | `12.5` |
|
||||||
|
| `end_time` | 浮點數 | 結束時間(秒) | `18.3` |
|
||||||
|
| `content` | JSONB | 詳細內容 | 見下方 |
|
||||||
|
| `vector_id` | 字串 (64) | 向量 ID | `vec_12345` |
|
||||||
|
| `text_content` | 文字 | 純文字內容 | `這是一段話` |
|
||||||
|
| `fps` | 浮點數 | 影片幀率 | `24.0` |
|
||||||
|
| `start_frame` | 整數 | 開始幀數 | `300` |
|
||||||
|
| `end_frame` | 整數 | 結束幀數 | `439` |
|
||||||
|
| `frame_count` | 整數 | 總幀數 | `139` |
|
||||||
|
|
||||||
|
### 2.2 Chunk 類型說明
|
||||||
|
|
||||||
|
| 類型 | ID | 說明 | 來源處理器 |
|
||||||
|
|------|-----|------|-----------|
|
||||||
|
| `sentence` | `sentence` | 語音轉文字片段 | ASR 處理 |
|
||||||
|
| `time` | `time_based` | 固定時間分段 | 系統自動切割 |
|
||||||
|
| `cut` | `cut` | 場景變化片段 | CUT 處理 |
|
||||||
|
| `trace` | `trace` | 軌跡追蹤片段 | YOLO 追蹤處理 |
|
||||||
|
| `story` | `story` | 故事線片段(父子區塊) | Story 分析處理 |
|
||||||
|
|
||||||
|
**父子區塊關係**:
|
||||||
|
- `story` 是**父區塊**,可包含多個 `sentence`、`cut`、`trace` 子區塊
|
||||||
|
- 透過 `parent_chunk_id` 和 `child_chunk_ids` 建立階層關係
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Content JSON 結構
|
||||||
|
|
||||||
|
每個 Chunk 的 `content` 欄位包含詳細的處理結果:
|
||||||
|
|
||||||
|
### 3.1 ASR Chunk(語音轉文字)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"text": "今天天氣非常好,我們去郊外踏青吧",
|
||||||
|
"words": [
|
||||||
|
{
|
||||||
|
"word": "今天",
|
||||||
|
"start": 12.5,
|
||||||
|
"end": 12.8,
|
||||||
|
"confidence": 0.95
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "天氣",
|
||||||
|
"start": 12.8,
|
||||||
|
"end": 13.1,
|
||||||
|
"confidence": 0.92
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"language": "zh-TW",
|
||||||
|
"speaker": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Cut Chunk(場景偵測)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scenes": [
|
||||||
|
{
|
||||||
|
"scene_id": "cut_001",
|
||||||
|
"start_time": 12.5,
|
||||||
|
"end_time": 45.2,
|
||||||
|
"transition": "cut",
|
||||||
|
"confidence": 0.98
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Trace Chunk(軌跡追蹤)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"track_id": "track_001",
|
||||||
|
"object_class": "person",
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"frame": 300,
|
||||||
|
"bbox": [120, 80, 200, 300],
|
||||||
|
"confidence": 0.95
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 301,
|
||||||
|
"bbox": [122, 82, 202, 302],
|
||||||
|
"confidence": 0.94
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_frames": 180
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Story Chunk(故事線)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"story_id": "story_001",
|
||||||
|
"title": "開場介紹",
|
||||||
|
"summary": "主持人介紹節目主題",
|
||||||
|
"child_chunk_ids": ["sentence_00001", "sentence_00002", "cut_00001"],
|
||||||
|
"tags": ["intro", "host"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 Metadata(其他偵測資訊)
|
||||||
|
|
||||||
|
人臉(Face)、文字辨識(OCR)、姿態(Pose)等偵測結果會附加在 `metadata` 欄位:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"faces": [
|
||||||
|
{
|
||||||
|
"bbox": [120, 80, 200, 180],
|
||||||
|
"confidence": 0.87,
|
||||||
|
"emotion": "neutral"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ocr": {
|
||||||
|
"text": "MOMENTRY",
|
||||||
|
"confidence": 0.96
|
||||||
|
},
|
||||||
|
"pose": {
|
||||||
|
"keypoints": [
|
||||||
|
{"name": "nose", "x": 192, "y": 85, "confidence": 0.95}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 時間格式說明
|
||||||
|
|
||||||
|
### 4.1 秒數格式(常用)
|
||||||
|
|
||||||
|
```
|
||||||
|
格式: 秒.幀數
|
||||||
|
範例: 1234.60 = 第 1234 秒 + 第 60 幀
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 時間軸格式
|
||||||
|
|
||||||
|
```
|
||||||
|
格式: HH:MM:SS.FF
|
||||||
|
範例: 00:20:34.12 = 20分34秒12幀
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 幀數計算
|
||||||
|
|
||||||
|
```
|
||||||
|
幀數 = 秒數 × fps
|
||||||
|
例如: 12.5秒 × 24fps = 300幀
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 實際資料範例
|
||||||
|
|
||||||
|
假設有一個影片,包含以下處理結果:
|
||||||
|
|
||||||
|
### 5.1 語音片段
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "952f5854b9febad1",
|
||||||
|
"chunk_id": "asr_00001",
|
||||||
|
"chunk_type": "sentence",
|
||||||
|
"start_time": 12.5,
|
||||||
|
"end_time": 18.3,
|
||||||
|
"content": {
|
||||||
|
"text": "今天天氣非常好,我們去郊外踏青吧",
|
||||||
|
"language": "zh-TW"
|
||||||
|
},
|
||||||
|
"text_content": "今天天氣非常好,我們去郊外踏青吧",
|
||||||
|
"start_frame": 300,
|
||||||
|
"end_frame": 439,
|
||||||
|
"fps": 24.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 場景片段
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "952f5854b9febad1",
|
||||||
|
"chunk_id": "cut_00001",
|
||||||
|
"chunk_type": "cut",
|
||||||
|
"start_time": 45.0,
|
||||||
|
"end_time": 120.5,
|
||||||
|
"content": {
|
||||||
|
"scenes": [{
|
||||||
|
"scene_id": "cut_001",
|
||||||
|
"transition": "cut",
|
||||||
|
"confidence": 0.98
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"start_frame": 1080,
|
||||||
|
"end_frame": 2892,
|
||||||
|
"fps": 24.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 如何使用 Chunk
|
||||||
|
|
||||||
|
### 6.1 API 取得 Chunk
|
||||||
|
|
||||||
|
使用搜尋 API 取得 Chunk:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://api.momentry.ddns.net/api/v1/search" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"query": "關鍵字",
|
||||||
|
"limit": 10
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**指定影片搜尋**:
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://api.momentry.ddns.net/api/v1/search" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"query": "關鍵字",
|
||||||
|
"uuid": "39567a0eb16f39fd",
|
||||||
|
"limit": 5
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 搜尋相關片段
|
||||||
|
|
||||||
|
當使用者搜尋「天氣」時,系統會:
|
||||||
|
|
||||||
|
1. 將「天氣」轉換為向量
|
||||||
|
2. 在向量資料庫中搜尋相似向量
|
||||||
|
3. 找到相關的 Chunk
|
||||||
|
4. 返回時間軸和內容
|
||||||
|
|
||||||
|
### 6.3 播放指定片段
|
||||||
|
|
||||||
|
取得 Chunk 後可播放:
|
||||||
|
|
||||||
|
```
|
||||||
|
開始時間: 12.5 秒
|
||||||
|
結束時間: 18.3 秒
|
||||||
|
影片 UUID: 39567a0eb16f39fd
|
||||||
|
```
|
||||||
|
|
||||||
|
**播放器連結格式**:
|
||||||
|
```
|
||||||
|
/player?uuid={uuid}&start={start_time}&end={end_time}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 組合多個 Chunk
|
||||||
|
|
||||||
|
多個相關 Chunk 可以組合成一個章節或故事線。
|
||||||
|
|
||||||
|
### 6.5 Story Chunk(父子關係)
|
||||||
|
|
||||||
|
Story Chunk 可包含多個子 Chunk:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"chunk_id": "story_001",
|
||||||
|
"chunk_type": "story",
|
||||||
|
"content": {
|
||||||
|
"story_id": "story_001",
|
||||||
|
"title": "開場介紹",
|
||||||
|
"child_chunk_ids": ["sentence_00001", "sentence_00002", "cut_00001"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. API 回應格式
|
||||||
|
|
||||||
|
### /search 回應
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"uuid": "39567a0eb16f39fd",
|
||||||
|
"chunk_id": "sentence_1471",
|
||||||
|
"chunk_type": "sentence",
|
||||||
|
"start_time": 5309.08,
|
||||||
|
"end_time": 5311.08,
|
||||||
|
"text": "influenced by a vital way,",
|
||||||
|
"score": 0.68
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"query": "關鍵字"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### /n8n/search 回應
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "關鍵字",
|
||||||
|
"count": 1,
|
||||||
|
"hits": [
|
||||||
|
{
|
||||||
|
"id": "sentence_1471",
|
||||||
|
"vid": "39567a0eb16f39fd",
|
||||||
|
"start": 5309.08,
|
||||||
|
"end": 5311.08,
|
||||||
|
"title": "Chunk sentence_1471",
|
||||||
|
"text": "influenced by a vital way,",
|
||||||
|
"score": 0.68,
|
||||||
|
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**: `file_path` 是影片的實際路徑,可用於本地播放。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 快速參考
|
||||||
|
|
||||||
|
| 項目 | 說明 |
|
||||||
|
|------|------|
|
||||||
|
| UUID | 影片唯一識別碼(16位 hex) |
|
||||||
|
| Chunk ID | 片段識別碼(如 `sentence_00001`) |
|
||||||
|
| chunk_type | 片段類型(sentence/time/cut/trace/story) |
|
||||||
|
| start_time | 開始時間(秒) |
|
||||||
|
| end_time | 結束時間(秒) |
|
||||||
|
| text_content | 純文字內容 |
|
||||||
|
| content | 詳細 JSON 結構 |
|
||||||
|
| metadata | 人臉、OCR、姿態等偵測結果 |
|
||||||
|
| parent_chunk_id | 父區塊 ID(用於 story 區塊) |
|
||||||
|
| child_chunk_ids | 子區塊 ID 列表(story 區塊專用) | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附錄:版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 內容 | 操作人 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| V1.0 | 2026-03-25 | 初版建立 | OpenCode |
|
||||||
|
| V1.1 | 2026-03-25 | 新增 API 取得 Chunk 方式、播放連結格式 | OpenCode |
|
||||||
+15
-3
@@ -2,9 +2,21 @@
|
|||||||
|
|
||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 版本 | V1.0 |
|
| 建立者 | OpenCode |
|
||||||
| 日期 | 2026-03-25 |
|
| 建立時間 | 2026-03-25 |
|
||||||
| 狀態 | 完成 |
|
| 文件版本 | V1.0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-25 | 創建示範手冊,包含 Demo API Key 與完整範例 | OpenCode | deepseek-reasoner |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**狀態**: 完成
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# Document Embedding Strategy - Parent-Child Chunks
|
# Document Embedding Strategy - Parent-Child Chunks
|
||||||
|
|
||||||
|
| Item | Content |
|
||||||
|
|------|---------|
|
||||||
|
| Author | Warren |
|
||||||
|
| Created | 2026-03-23 |
|
||||||
|
| Document Version | V1.0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Purpose | Operator | Tool/Model |
|
||||||
|
|---------|------|---------|----------|------------|
|
||||||
|
| V1.0 | 2026-03-23 | Create document embedding strategy | Warren | OpenCode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Momentry uses a **parent-child chunk hierarchy** for improved RAG retrieval. This document describes the embedding strategy for this hierarchy.
|
Momentry uses a **parent-child chunk hierarchy** for improved RAG retrieval. This document describes the embedding strategy for this hierarchy.
|
||||||
@@ -44,7 +60,7 @@ embedding_text = f"Summary: {parent.text_content}
|
|||||||
Children: {child_text_1}. {child_text_2}. {child_text_3}..."
|
Children: {child_text_1}. {child_text_2}. {child_text_3}..."
|
||||||
```
|
```
|
||||||
|
|
||||||
**Prefix**: `search_document: ` (for documents in Qdrant)
|
**Prefix**: `search_document:` (for documents in Qdrant)
|
||||||
|
|
||||||
**Example**:
|
**Example**:
|
||||||
```
|
```
|
||||||
@@ -58,7 +74,7 @@ embedding_text = f"[{child.chunk_type}] {child.text_content}
|
|||||||
Parent: {parent.description}"
|
Parent: {parent.description}"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Prefix**: `search_document: `
|
**Prefix**: `search_document:`
|
||||||
|
|
||||||
**Example**:
|
**Example**:
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -461,4 +461,4 @@ sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
|||||||
- `docs/INSTALL_POSTGRESQL.md` - PostgreSQL 安裝
|
- `docs/INSTALL_POSTGRESQL.md` - PostgreSQL 安裝
|
||||||
- `docs/INSTALL_REDIS.md` - Redis 安裝
|
- `docs/INSTALL_REDIS.md` - Redis 安裝
|
||||||
- `docs/INSTALL_QDRANT.md` - Qdrant 安裝
|
- `docs/INSTALL_QDRANT.md` - Qdrant 安裝
|
||||||
- `docs/PENDING_ISSUES.md` - 待解決問題
|
- `docs/PENDING_ISSUES.md` - 待解決問題
|
||||||
|
|||||||
@@ -527,13 +527,13 @@ SFTPGo 提供 RESTful API 用於管理用戶和組,支援自動化運維。
|
|||||||
|
|
||||||
### API 認證方式
|
### API 認證方式
|
||||||
|
|
||||||
1. **獲取 Access Token** (使用 Basic Auth):
|
- **獲取 Access Token** (使用 Basic Auth):
|
||||||
```bash
|
```bash
|
||||||
TOKEN=$(curl -s -X GET http://localhost:8080/api/v2/token \
|
TOKEN=$(curl -s -X GET http://localhost:8080/api/v2/token \
|
||||||
-u "admin:Test3200Test3200" | jq -r '.access_token')
|
-u "admin:Test3200Test3200" | jq -r '.access_token')
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **使用 Token 調用 API**:
|
- **使用 Token 調用 API**:
|
||||||
```bash
|
```bash
|
||||||
curl -X GET http://localhost:8080/api/v2/admins \
|
curl -X GET http://localhost:8080/api/v2/admins \
|
||||||
-H "Authorization: Bearer $TOKEN"
|
-H "Authorization: Bearer $TOKEN"
|
||||||
@@ -569,7 +569,7 @@ curl -s -X POST http://localhost:8080/api/v2/users \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
**權限格式注意**: 必須為 map[string][]string 格式,例如:
|
**權限格式注意**: 必須為 `map[string][]string` 格式,例如:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"/": ["*"],
|
"/": ["*"],
|
||||||
@@ -752,12 +752,12 @@ sftpgo serve --config-file /Users/accusys/momentry/etc/sftpgo/sftpgo.json &
|
|||||||
|
|
||||||
### Hook 故障排除
|
### Hook 故障排除
|
||||||
|
|
||||||
1. **檢查 Hook 日誌**:
|
- **檢查 Hook 日誌**:
|
||||||
```bash
|
```bash
|
||||||
tail -f /Users/accusys/sftpgo_test/hook.log
|
tail -f /Users/accusys/sftpgo_test/hook.log
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **手動測試 Hook 腳本**:
|
- **手動測試 Hook 腳本**:
|
||||||
```bash
|
```bash
|
||||||
export SFTPGO_USERNAME=demo
|
export SFTPGO_USERNAME=demo
|
||||||
export SFTPGO_FILEPATH="./test.txt"
|
export SFTPGO_FILEPATH="./test.txt"
|
||||||
@@ -766,7 +766,7 @@ export SFTPGO_ACTION=add
|
|||||||
/Users/accusys/sftpgo_test/register_hook.sh
|
/Users/accusys/sftpgo_test/register_hook.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **SFTPGo 錯誤日誌**:
|
- **SFTPGo 錯誤日誌**:
|
||||||
```bash
|
```bash
|
||||||
tail -20 /Users/accusys/momentry/log/sftpgo.error.log
|
tail -20 /Users/accusys/momentry/log/sftpgo.error.log
|
||||||
```
|
```
|
||||||
@@ -877,12 +877,12 @@ sftp> put test.txt
|
|||||||
|
|
||||||
## 常見問題
|
## 常見問題
|
||||||
|
|
||||||
#### "無效的憑證" 即使密碼正確
|
### "無效的憑證" 即使密碼正確
|
||||||
|
|
||||||
- PostgreSQL 中的密碼哈希可能不符合 SFTPGo 預期格式
|
- PostgreSQL 中的密碼哈希可能不符合 SFTPGo 預期格式
|
||||||
- 使用 Web 面板的 **Forgot password** 功能而非直接 SQL 更新
|
- 使用 Web 面板的 **Forgot password** 功能而非直接 SQL 更新
|
||||||
|
|
||||||
#### CSRF Token 錯誤
|
### CSRF Token 錯誤
|
||||||
|
|
||||||
- 清除瀏覽器中 `localhost:8080` 的 cookies
|
- 清除瀏覽器中 `localhost:8080` 的 cookies
|
||||||
- 使用無痕/私密瀏覽視窗
|
- 使用無痕/私密瀏覽視窗
|
||||||
|
|||||||
@@ -779,4 +779,4 @@ log_info "✅ 部署完成!"
|
|||||||
|
|
||||||
**負責人**: OpenCode AI Assistant
|
**負責人**: OpenCode AI Assistant
|
||||||
|
|
||||||
**最後更新**: 2026-03-23
|
**最後更新**: 2026-03-23
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
# Momentry Core 影片 RAG 系統說明稿
|
# Momentry Core 影片 RAG 系統說明稿
|
||||||
|
|
||||||
|
| 項目 | 內容 |
|
||||||
|
|------|------|
|
||||||
|
| 建立者 | Warren |
|
||||||
|
| 建立時間 | 2026-03-22 |
|
||||||
|
| 文件版本 | V1.1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-22 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||||
|
| V1.1 | 2026-03-25 | 更新API回應格式 (media_url→file_path) 與認證標頭 | OpenCode | deepseek-reasoner |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 系統架構
|
## 系統架構
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -85,22 +102,9 @@ POST http://localhost:3002/api/v1/search
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**回應:**
|
> **注意**:
|
||||||
```json
|
> 1. **API 認證**: 所有 `/api/v1/*` 端點需要 `X-API-Key` 標頭
|
||||||
{
|
> 2. **檔案路徑轉換**: API 現在返回 `file_path`(檔案系統路徑),需要轉換為可訪問的 URL(例如透過 SFTPGo 分享連結)
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"uuid": "a1b10138a6bbb0cd",
|
|
||||||
"chunk_id": "sentence_0006",
|
|
||||||
"chunk_type": "sentence",
|
|
||||||
"start_time": 48.8,
|
|
||||||
"end_time": 55.44,
|
|
||||||
"text": "fun plot twists, Woody Dialog and charming performances...",
|
|
||||||
"score": 0.526
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -132,7 +136,7 @@ POST http://localhost:3002/api/v1/n8n/search
|
|||||||
"title": "Chunk sentence_0006",
|
"title": "Chunk sentence_0006",
|
||||||
"text": "fun plot twists...",
|
"text": "fun plot twists...",
|
||||||
"score": 0.526,
|
"score": 0.526,
|
||||||
"media_url": "https://wp.momentry.ddns.net/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
|
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -187,6 +191,7 @@ POST http://localhost:3002/api/v1/n8n/search
|
|||||||
Method: POST
|
Method: POST
|
||||||
URL: http://localhost:3002/api/v1/n8n/search
|
URL: http://localhost:3002/api/v1/n8n/search
|
||||||
Body Content Type: JSON
|
Body Content Type: JSON
|
||||||
|
Headers: X-API-Key (需設定)
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Body:
|
3. Body:
|
||||||
@@ -215,12 +220,17 @@ const results = hits.map((hit, index) => ({
|
|||||||
text: hit.text,
|
text: hit.text,
|
||||||
time: `${hit.start}s - ${hit.end}s`,
|
time: `${hit.start}s - ${hit.end}s`,
|
||||||
score: Math.round(hit.score * 100) + "%",
|
score: Math.round(hit.score * 100) + "%",
|
||||||
url: hit.media_url + "#t=" + hit.start + "," + hit.end
|
// 注意: API 現在返回 file_path(檔案系統路徑),需要轉換為可訪問的 URL
|
||||||
|
url: hit.file_path + "#t=" + hit.start + "," + hit.end // 需實作檔案路徑轉換為 URL
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { json: { results } };
|
return { json: { results } };
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **注意**:
|
||||||
|
> 1. **API 認證**: 所有 `/api/v1/*` 端點需要 `X-API-Key` 標頭
|
||||||
|
> 2. **檔案路徑轉換**: API 現在返回 `file_path`(檔案系統路徑),需要轉換為可訪問的 URL(例如透過 SFTPGo 分享連結)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 4: 格式化輸出
|
### Step 4: 格式化輸出
|
||||||
@@ -248,18 +258,20 @@ return { json: { results } };
|
|||||||
# 語意搜尋
|
# 語意搜尋
|
||||||
curl -X POST http://localhost:3002/api/v1/search \
|
curl -X POST http://localhost:3002/api/v1/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"query": "charade", "limit": 3}'
|
-d '{"query": "charade", "limit": 3}'
|
||||||
|
|
||||||
# n8n 格式
|
# n8n 格式
|
||||||
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"query": "charade", "limit": 3}'
|
-d '{"query": "charade", "limit": 3}'
|
||||||
|
|
||||||
# 影片列表
|
# 影片列表
|
||||||
curl http://localhost:3002/api/v1/videos
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||||
|
|
||||||
# 特定影片區塊
|
# 特定影片區塊
|
||||||
curl http://localhost:3002/api/v1/videos/a1b10138a6bbb0cd/chunks
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos/a1b10138a6bbb0cd/chunks
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+34
-5
@@ -1,11 +1,29 @@
|
|||||||
# n8n 整合範例
|
# n8n 整合範例
|
||||||
|
|
||||||
|
| 項目 | 內容 |
|
||||||
|
|------|------|
|
||||||
|
| 建立者 | Warren |
|
||||||
|
| 建立時間 | 2026-03-18 |
|
||||||
|
| 文件版本 | V1.1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||||
|
| V1.1 | 2026-03-25 | 更新API回應格式 (media_url→file_path) 與認證標頭 | OpenCode | deepseek-reasoner |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 基本設定
|
## 基本設定
|
||||||
|
|
||||||
### API 端點
|
### API 端點
|
||||||
- **Base URL:** `http://localhost:3002/api/v1`
|
- **Base URL:** `http://localhost:3002/api/v1`
|
||||||
- **Method:** `POST`
|
- **Method:** `POST`
|
||||||
- **Content-Type:** `application/json`
|
- **Content-Type:** `application/json`
|
||||||
|
- **Authentication:** `X-API-Key: YOUR_API_KEY` (所有 `/api/v1/*` 端點皆需要)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -36,7 +54,8 @@
|
|||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"headers": {
|
"headers": {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
|
"X-API-Key": "YOUR_API_KEY"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +81,7 @@ return results.map(r => ({
|
|||||||
|
|
||||||
## Workflow 2: n8n 專用格式
|
## Workflow 2: n8n 專用格式
|
||||||
|
|
||||||
使用 `/n8n/search` 端點(已包含 media_url)
|
使用 `/n8n/search` 端點(已包含 file_path)
|
||||||
|
|
||||||
### HTTP Request
|
### HTTP Request
|
||||||
```json
|
```json
|
||||||
@@ -72,6 +91,12 @@ return results.map(r => ({
|
|||||||
"body": {
|
"body": {
|
||||||
"query": "={{ $json.searchTerm }}",
|
"query": "={{ $json.searchTerm }}",
|
||||||
"limit": 5
|
"limit": 5
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-API-Key": "YOUR_API_KEY"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -90,12 +115,14 @@ return results.map(r => ({
|
|||||||
"title": "Chunk sentence_0006",
|
"title": "Chunk sentence_0006",
|
||||||
"text": "fun plot twists...",
|
"text": "fun plot twists...",
|
||||||
"score": 0.526,
|
"score": 0.526,
|
||||||
"media_url": "https://wp.momentry.ddns.net/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
|
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **注意**: API 現在返回 `file_path`(檔案系統路徑)而非 `media_url`(網頁 URL)。如需在網頁中播放影片,請將檔案路徑轉換為可訪問的 URL(例如透過 SFTPGo 分享連結)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Workflow 3: 訊息機器人整合
|
## Workflow 3: 訊息機器人整合
|
||||||
@@ -205,16 +232,18 @@ return {
|
|||||||
# 基本搜尋
|
# 基本搜尋
|
||||||
curl -X POST http://localhost:3002/api/v1/search \
|
curl -X POST http://localhost:3002/api/v1/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"query": "charade", "limit": 3}'
|
-d '{"query": "charade", "limit": 3}'
|
||||||
|
|
||||||
# n8n 格式
|
# n8n 格式
|
||||||
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"query": "charade", "limit": 3}'
|
-d '{"query": "charade", "limit": 3}'
|
||||||
|
|
||||||
# 取得影片列表
|
# 取得影片列表
|
||||||
curl http://localhost:3002/api/v1/videos
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||||
|
|
||||||
# 取得特定影片的區塊
|
# 取得特定影片的區塊
|
||||||
curl http://localhost:3002/api/v1/videos/a1b10138a6bbb0cd/chunks
|
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos/a1b10138a6bbb0cd/chunks
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
# n8n Video RAG Demo - API 執行記錄
|
# n8n Video RAG Demo - API 執行記錄
|
||||||
|
|
||||||
> 建立時間: 2026-03-22
|
| 項目 | 內容 |
|
||||||
> 目標: 完整執行 n8n Video RAG Workflow 並記錄所有 API 呼叫
|
|------|------|
|
||||||
|
| 建立者 | Warren |
|
||||||
|
| 建立時間 | 2026-03-22 |
|
||||||
|
| 文件版本 | V1.1 |
|
||||||
|
| 目標 | 完整執行 n8n Video RAG Workflow 並記錄所有 API 呼叫 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-22 | 創建文件 | Warren | OpenCode |
|
||||||
|
| V1.1 | 2026-03-26 | 更新 API 範例,新增 X-API-Key 驗證標頭 | OpenCode | deepseek-reasoner |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -297,12 +310,13 @@ Content-Type: application/json
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 4.2: n8n 搜尋 (含 media_url)
|
### Step 4.2: n8n 搜尋 (含 file_path)
|
||||||
|
|
||||||
**API 呼叫:**
|
**API 呼叫:**
|
||||||
```bash
|
```bash
|
||||||
curl -X POST "http://localhost:3002/api/v1/n8n/search" \
|
curl -X POST "http://localhost:3002/api/v1/n8n/search" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: demo_api_key_12345" \
|
||||||
-d '{
|
-d '{
|
||||||
"query": "What is the movie about?",
|
"query": "What is the movie about?",
|
||||||
"limit": 10,
|
"limit": 10,
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
# n8n Video RAG Workflow - Node 設計
|
# n8n Video RAG Workflow - Node 設計
|
||||||
|
|
||||||
> 建立時間: 2026-03-22
|
| 項目 | 內容 |
|
||||||
> 目標: 讓 marcom 團隊能夠複製、貼上、修改使用的完整操作指南
|
|------|------|
|
||||||
|
| 建立者 | Warren |
|
||||||
|
| 建立時間 | 2026-03-22 |
|
||||||
|
| 文件版本 | V1.1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-22 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||||
|
| V1.1 | 2026-03-25 | 更新API回應格式 (media_url→file_path) 與認證標頭 | OpenCode | deepseek-reasoner |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -117,7 +129,7 @@
|
|||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ ⑫ Natural Language Search │ │
|
│ │ ⑫ Natural Language Search │ │
|
||||||
│ │ ↓ │ │
|
│ │ ↓ │ │
|
||||||
│ │ ⑬ Get Media URL (含 media_url) │ │
|
│ │ ⑬ Get File Path (含 file_path) │ │
|
||||||
│ │ ↓ │ │
|
│ │ ↓ │ │
|
||||||
│ │ ⑭ Build Response │ │
|
│ │ ⑭ Build Response │ │
|
||||||
│ │ ↓ │ │
|
│ │ ↓ │ │
|
||||||
@@ -363,7 +375,7 @@ Output:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**注意**: 只有 asr、asrx、story 三個模組
|
> **注意**: API 現在返回 `file_path`(檔案系統路徑)而非 `media_url`(網頁 URL)。如需在網頁中播放影片,請將檔案路徑轉換為可訪問的 URL(例如透過 SFTPGo 分享連結)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -559,7 +571,7 @@ Output:
|
|||||||
"vid": "a1b10138a6bbb0cd",
|
"vid": "a1b10138a6bbb0cd",
|
||||||
"text": "Hello and welcome to the old-time movie show...",
|
"text": "Hello and welcome to the old-time movie show...",
|
||||||
"score": 0.92,
|
"score": 0.92,
|
||||||
"media_url": "https://wp.momentry.ddns.net/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
|
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -642,12 +654,13 @@ Configuration:
|
|||||||
| 項目 | 值 |
|
| 項目 | 值 |
|
||||||
|------|-----|
|
|------|-----|
|
||||||
| API Base | `http://localhost:3002` |
|
| API Base | `http://localhost:3002` |
|
||||||
|
| Authentication | `X-API-Key` header (所有 `/api/v1/*` 端點) |
|
||||||
| Register | `POST /api/v1/register` |
|
| Register | `POST /api/v1/register` |
|
||||||
| Progress | `GET /api/v1/progress/{uuid}` |
|
| Progress | `GET /api/v1/progress/{uuid}` |
|
||||||
| Search | `POST /api/v1/search` |
|
| Search | `POST /api/v1/search` |
|
||||||
| n8n Search | `POST /api/v1/n8n/search` |
|
| n8n Search | `POST /api/v1/n8n/search` |
|
||||||
| Hybrid Search | `POST /api/v1/search/hybrid` |
|
| Hybrid Search | `POST /api/v1/search/hybrid` |
|
||||||
| Media Base | `https://wp.momentry.ddns.net` |
|
| Media Base | `https://wp.momentry.ddns.net` (僅供參考,API 返回 `file_path` 而非 URL) |
|
||||||
|
|
||||||
### Demo 測試資料
|
### Demo 測試資料
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
# n8n HTTP Request Node 設定指南
|
# n8n HTTP Request Node 設定指南
|
||||||
|
|
||||||
|
| 項目 | 內容 |
|
||||||
|
|------|------|
|
||||||
|
| 建立者 | OpenCode |
|
||||||
|
| 建立時間 | 2026-03-26 |
|
||||||
|
| 文件版本 | V1.1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-23 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||||
|
| V1.1 | 2026-03-26 | 新增 API Key 驗證說明,更新 curl 範例 | OpenCode | deepseek-reasoner |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
> **API URL 說明**:
|
> **API URL 說明**:
|
||||||
> - **本地測試**: `http://localhost:3002`
|
> - **本地測試**: `http://localhost:3002`
|
||||||
> - **n8n workflow**: `https://api.momentry.ddns.net`
|
> - **n8n workflow**: `https://api.momentry.ddns.net`
|
||||||
@@ -32,7 +49,9 @@ Node: HTTP Request
|
|||||||
│ "query": "={{ $json.query }}",
|
│ "query": "={{ $json.query }}",
|
||||||
│ "limit": "={{ $json.limit }}"
|
│ "limit": "={{ $json.limit }}"
|
||||||
│ }
|
│ }
|
||||||
└── Options: (empty)
|
├── Send Headers: ✓ (checked)
|
||||||
|
└── Header Parameters:
|
||||||
|
└── X-API-Key: {{ $env.MOMENTRY_API_KEY }}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 方法 2: 使用 Raw Body + Headers
|
### 方法 2: 使用 Raw Body + Headers
|
||||||
@@ -51,7 +70,8 @@ Node: HTTP Request
|
|||||||
│ }
|
│ }
|
||||||
├── Send Headers: ✓ (checked)
|
├── Send Headers: ✓ (checked)
|
||||||
└── Header Parameters:
|
└── Header Parameters:
|
||||||
└── Content-Type: application/json
|
├── Content-Type: application/json
|
||||||
|
└── X-API-Key: {{ $env.MOMENTRY_API_KEY }}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 方法 3: 最簡單的 Hardcoded 測試
|
### 方法 3: 最簡單的 Hardcoded 測試
|
||||||
@@ -218,8 +238,12 @@ URL: https://api.momentry.ddns.net/api/v1/n8n/search
|
|||||||
|
|
||||||
在終端機測試:
|
在終端機測試:
|
||||||
```bash
|
```bash
|
||||||
|
# 需要 API Key 驗證 (設定環境變數或直接替換)
|
||||||
|
export MOMENTRY_API_KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
|
||||||
|
|
||||||
curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \
|
curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: $MOMENTRY_API_KEY" \
|
||||||
-d '{"query":"charade","limit":2}'
|
-d '{"query":"charade","limit":2}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,22 @@
|
|||||||
|
|
||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 版本 | V1.1 |
|
| 建立者 | Warren |
|
||||||
| 日期 | 2026-03-23 |
|
| 建立時間 | 2026-03-23 |
|
||||||
| 目標讀者 | n8n 使用者、DevOps |
|
| 文件版本 | V1.1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-22 | 創建 n8n 整合手冊 | Warren | OpenCode |
|
||||||
|
| V1.1 | 2026-03-23 | 新增 API Key 驗證與完整工作流範例 | Warren | OpenCode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**目標讀者**: n8n 使用者、DevOps
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# OpenCode n8n MCP 整合設定
|
# OpenCode n8n MCP 整合設定
|
||||||
|
|
||||||
|
| 項目 | 內容 |
|
||||||
|
|------|------|
|
||||||
|
| 建立者 | Warren |
|
||||||
|
| 建立時間 | 2026-03-23 |
|
||||||
|
| 文件版本 | V1.0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-23 | 創建 n8n MCP 整合設定文件 | Warren | OpenCode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
> 建立時間: 2026-03-23
|
> 建立時間: 2026-03-23
|
||||||
> 更新時間: 2026-03-23
|
> 更新時間: 2026-03-23
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# n8n MCP 整合測試報告
|
# n8n MCP 整合測試報告
|
||||||
|
|
||||||
|
| 項目 | 內容 |
|
||||||
|
|------|------|
|
||||||
|
| 建立者 | Warren |
|
||||||
|
| 建立時間 | 2026-03-23 |
|
||||||
|
| 文件版本 | V1.0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-23 | 創建測試報告 | Warren | OpenCode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 測試日期
|
## 測試日期
|
||||||
2026-03-23
|
2026-03-23
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
# n8n Video Search 工作流程 - 成功設定指南
|
# n8n Video Search 工作流程 - 成功設定指南
|
||||||
|
|
||||||
> 建立時間: 2026-03-22
|
| 項目 | 內容 |
|
||||||
> 適用版本: n8n 2.3.5
|
|------|------|
|
||||||
|
| 建立者 | Warren |
|
||||||
|
| 建立時間 | 2026-03-22 |
|
||||||
|
| 文件版本 | V1.1 |
|
||||||
|
| 適用版本 | n8n 2.3.5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-22 | 創建文件 | Warren | OpenCode |
|
||||||
|
| V1.1 | 2026-03-26 | 更新 API 範例,新增 X-API-Key 驗證標頭 | OpenCode | deepseek-reasoner |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -27,7 +40,11 @@
|
|||||||
"sendBody": true,
|
"sendBody": true,
|
||||||
"specifyBody": "json",
|
"specifyBody": "json",
|
||||||
"jsonBody": "{\"query\":\"charade\",\"limit\":3}",
|
"jsonBody": "{\"query\":\"charade\",\"limit\":3}",
|
||||||
"options": {}
|
"options": {
|
||||||
|
"headers": {
|
||||||
|
"X-API-Key": "demo_api_key_12345"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -85,7 +102,11 @@
|
|||||||
"sendBody": true,
|
"sendBody": true,
|
||||||
"specifyBody": "json",
|
"specifyBody": "json",
|
||||||
"jsonBody": "{\"query\":\"charade\",\"limit\":3}",
|
"jsonBody": "{\"query\":\"charade\",\"limit\":3}",
|
||||||
"options": {}
|
"options": {
|
||||||
|
"headers": {
|
||||||
|
"X-API-Key": "demo_api_key_12345"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"name": "Search API",
|
"name": "Search API",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
@@ -157,7 +178,7 @@
|
|||||||
"title": "Chunk sentence_0006",
|
"title": "Chunk sentence_0006",
|
||||||
"text": "fun plot twists, Woody Dialog and charming performances...",
|
"text": "fun plot twists, Woody Dialog and charming performances...",
|
||||||
"score": 0.526,
|
"score": 0.526,
|
||||||
"media_url": "https://wp.momentry.ddns.net/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
|
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -203,13 +224,14 @@
|
|||||||
```bash
|
```bash
|
||||||
curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \
|
curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: demo_api_key_12345" \
|
||||||
-d '{"query":"charade","limit":3}'
|
-d '{"query":"charade","limit":3}'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 驗證服務狀態
|
### 驗證服務狀態
|
||||||
```bash
|
```bash
|
||||||
# 檢查 Momentry Core
|
# 檢查 Momentry Core
|
||||||
curl https://api.momentry.ddns.net/api/v1/videos
|
curl -H "X-API-Key: demo_api_key_12345" https://api.momentry.ddns.net/api/v1/videos
|
||||||
|
|
||||||
# 檢查 n8n
|
# 檢查 n8n
|
||||||
curl http://localhost:5678/api/v1/workflows \
|
curl http://localhost:5678/api/v1/workflows \
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# Playground Binary Implementation Plan
|
# Playground Binary Implementation Plan
|
||||||
|
|
||||||
|
| Item | Content |
|
||||||
|
|------|---------|
|
||||||
|
| Author | Warren |
|
||||||
|
| Created | 2026-03-23 |
|
||||||
|
| Document Version | V1.0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Purpose | Operator | Tool/Model |
|
||||||
|
|---------|------|---------|----------|------------|
|
||||||
|
| V1.0 | 2026-03-23 | Create implementation plan | Warren | OpenCode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Create separate `momentry_playground` binary with distinct configuration from `momentry` (production).
|
Create separate `momentry_playground` binary with distinct configuration from `momentry` (production).
|
||||||
|
|||||||
+36
-15
@@ -1,6 +1,19 @@
|
|||||||
# Video Processing Pipeline - 處理流程
|
# Video Processing Pipeline - 處理流程
|
||||||
|
|
||||||
> 建立時間: 2026-03-22
|
| 項目 | 內容 |
|
||||||
|
|------|------|
|
||||||
|
| 建立者 | Warren |
|
||||||
|
| 建立時間 | 2026-03-22 |
|
||||||
|
| 文件版本 | V1.1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-22 | 創建文件 | Warren | OpenCode |
|
||||||
|
| V1.1 | 2026-03-26 | 更新流程圖文字 (media_url→file_path) | OpenCode | deepseek-reasoner |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -54,7 +67,7 @@
|
|||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ Natural Language Query ──→ [Embedding] ──→ [Qdrant Search] │ │
|
│ │ Natural Language Query ──→ [Embedding] ──→ [Qdrant Search] │ │
|
||||||
│ │ ↓ │ │
|
│ │ ↓ │ │
|
||||||
│ │ 返回結果含 media_url │ │
|
│ │ 返回結果含 file_path │ │
|
||||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
@@ -106,11 +119,11 @@ cargo run --bin momentry -- chunk <uuid>
|
|||||||
### Stage 4: 向量化
|
### Stage 4: 向量化
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 向量化 chunks
|
# 向量化 chunks(使用預設模型 nomic-embed-text-v2-moe:latest)
|
||||||
cargo run --bin momentry -- vectorize <uuid>
|
cargo run --bin momentry -- vectorize <uuid>
|
||||||
|
|
||||||
# 指定模型
|
# 明確指定模型
|
||||||
cargo run --bin momentry -- vectorize <uuid> --model sentence-transformers/all-MiniLM-L6-v2
|
cargo run --bin momentry -- vectorize <uuid> --model nomic-embed-text-v2-moe:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -174,18 +187,27 @@ YOLO: ✓ Already complete, skipping
|
|||||||
|
|
||||||
## 向量化模型選擇
|
## 向量化模型選擇
|
||||||
|
|
||||||
|
### 統一嵌入模型
|
||||||
|
Momentry Core 統一使用 **`nomic-embed-text-v2-moe:latest`** 作為所有規則的嵌入模型:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 預設模型
|
# 統一模型(所有 Rule 1/2/3 使用)
|
||||||
--model sentence-transformers/all-MiniLM-L6-v2
|
--model nomic-embed-text-v2-moe:latest
|
||||||
|
```
|
||||||
|
|
||||||
# 高精度模型
|
### 模型特性
|
||||||
--model sentence-transformers/all-mpnet-base-v2
|
| 特性 | 說明 |
|
||||||
|
|------|------|
|
||||||
|
| **模型名稱** | `nomic-embed-text-v2-moe:latest` |
|
||||||
|
| **向量維度** | 768 維 |
|
||||||
|
| **多語言支持** | ✅ 完整支持(英語、中文、日語、韓語等) |
|
||||||
|
| **模型架構** | Mixture of Experts (MoE) |
|
||||||
|
| **推理速度** | 快速,適合實時應用 |
|
||||||
|
|
||||||
# 多語言模型
|
### 使用方式
|
||||||
--model sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
|
```bash
|
||||||
|
# 向量化命令
|
||||||
# 中文模型
|
cargo run --bin momentry -- vectorize <uuid> --model nomic-embed-text-v2-moe:latest
|
||||||
--model sentence-transformers/paraphrase-multilingual-mpnet-base-v2
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -268,4 +290,3 @@ curl http://localhost:3002/api/v1/progress/{uuid}
|
|||||||
3. **獨立 Chunk 命令** - 分離 chunk 生成
|
3. **獨立 Chunk 命令** - 分離 chunk 生成
|
||||||
4. **獨立 Vectorize 命令** - 分離向量化流程
|
4. **獨立 Vectorize 命令** - 分離向量化流程
|
||||||
5. **模型管理** - 新增、選擇、預覽模型
|
5. **模型管理** - 新增、選擇、預覽模型
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
| 項目 | 值 |
|
| 項目 | 值 |
|
||||||
|------|-----|
|
|------|-----|
|
||||||
| **主機** | `momentry.ddns.net` |
|
| **主機** | `sftpgo.momentry.ddns.net` |
|
||||||
| **SFTP 連接埠** | `2022` |
|
| **SFTP 連接埠** | `2022` |
|
||||||
| **用戶名** | `demo` |
|
| **用戶名** | `demo` |
|
||||||
| **密碼** | `demopassword123` |
|
| **密碼** | `demopassword123` |
|
||||||
@@ -36,15 +36,15 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 使用密碼連線
|
# 使用密碼連線
|
||||||
sshpass -p "demopassword123" sftp -P 2022 demo@momentry.ddns.net
|
sshpass -p "demopassword123" sftp -P 2022 demo@sftpgo.momentry.ddns.net
|
||||||
|
|
||||||
# 使用金鑰連線 (需先設定)
|
# 使用金鑰連線 (需先設定)
|
||||||
sftp -P 2022 -i ~/.ssh/id_rsa demo@momentry.ddns.net
|
sftp -P 2022 -i ~/.ssh/id_rsa demo@sftpgo.momentry.ddns.net
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. FileZilla
|
### 2. FileZilla
|
||||||
|
|
||||||
1. **主機**: `sftp://momentry.ddns.net`
|
1. **主機**: `sftp://sftpgo.momentry.ddns.net`
|
||||||
2. **連接埠**: `2022`
|
2. **連接埠**: `2022`
|
||||||
3. **協定**: `SFTP`
|
3. **協定**: `SFTP`
|
||||||
4. **登入類型**: `一般`
|
4. **登入類型**: `一般`
|
||||||
@@ -55,7 +55,7 @@ sftp -P 2022 -i ~/.ssh/id_rsa demo@momentry.ddns.net
|
|||||||
|
|
||||||
1. 選擇 **連線 > 新連線**
|
1. 選擇 **連線 > 新連線**
|
||||||
2. 協定選擇 **SFTP (SSH File Transfer Protocol)**
|
2. 協定選擇 **SFTP (SSH File Transfer Protocol)**
|
||||||
3. 伺服器: `momentry.ddns.net`
|
3. 伺服器: `sftpgo.momentry.ddns.net`
|
||||||
4. 連接埠: `2022`
|
4. 連接埠: `2022`
|
||||||
5. 使用者名稱: `demo`
|
5. 使用者名稱: `demo`
|
||||||
6. 密碼: `demopassword123`
|
6. 密碼: `demopassword123`
|
||||||
@@ -65,7 +65,7 @@ sftp -P 2022 -i ~/.ssh/id_rsa demo@momentry.ddns.net
|
|||||||
```bash
|
```bash
|
||||||
curl -u demo:demopassword123 \
|
curl -u demo:demopassword123 \
|
||||||
-T /path/to/video.mp4 \
|
-T /path/to/video.mp4 \
|
||||||
sftp://momentry.ddns.net:2022/demo/
|
sftp://sftpgo.momentry.ddns.net:2022/demo/
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -76,7 +76,7 @@ curl -u demo:demopassword123 \
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 進入互動式模式
|
# 進入互動式模式
|
||||||
sftp demo@momentry.ddns.net -P 2022
|
sftp demo@sftpgo.momentry.ddns.net -P 2022
|
||||||
|
|
||||||
# 常用指令
|
# 常用指令
|
||||||
sftp> pwd # 顯示目前目錄
|
sftp> pwd # 顯示目前目錄
|
||||||
@@ -94,7 +94,7 @@ sftp> exit # 斷線
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 上傳多個檔案
|
# 上傳多個檔案
|
||||||
sshpass -p "demopassword123" sftp -P 2022 demo@momentry.ddns.net <<EOF
|
sshpass -p "demopassword123" sftp -P 2022 demo@sftpgo.momentry.ddns.net <<EOF
|
||||||
cd uploads
|
cd uploads
|
||||||
put video1.mp4
|
put video1.mp4
|
||||||
put video2.mp4
|
put video2.mp4
|
||||||
@@ -103,7 +103,7 @@ bye
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# 使用 glob 上傳
|
# 使用 glob 上傳
|
||||||
sshpass -p "demopassword123" sftp -P 2022 demo@momentry.ddns.net <<EOF
|
sshpass -p "demopassword123" sftp -P 2022 demo@sftpgo.momentry.ddns.net <<EOF
|
||||||
mput /path/to/videos/*.mp4
|
mput /path/to/videos/*.mp4
|
||||||
bye
|
bye
|
||||||
EOF
|
EOF
|
||||||
@@ -119,7 +119,7 @@ EOF
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# upload.sh - 上傳視頻到 Momentry
|
# upload.sh - 上傳視頻到 Momentry
|
||||||
|
|
||||||
HOST="momentry.ddns.net"
|
HOST="sftpgo.momentry.ddns.net"
|
||||||
PORT="2022"
|
PORT="2022"
|
||||||
USER="demo"
|
USER="demo"
|
||||||
PASS="demopassword123"
|
PASS="demopassword123"
|
||||||
@@ -160,7 +160,7 @@ import sys
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
def upload_file(local_path, remote_dir="/demo/uploads"):
|
def upload_file(local_path, remote_dir="/demo/uploads"):
|
||||||
host = "momentry.ddns.net"
|
host = "sftpgo.momentry.ddns.net"
|
||||||
port = 2022
|
port = 2022
|
||||||
username = "demo"
|
username = "demo"
|
||||||
password = "demopassword123"
|
password = "demopassword123"
|
||||||
@@ -250,7 +250,7 @@ curl -u demo:demopassword123 \
|
|||||||
|
|
||||||
上傳目錄可能需要先建立:
|
上傳目錄可能需要先建立:
|
||||||
```bash
|
```bash
|
||||||
sshpass -p "demopassword123" sftp -P 2022 demo@momentry.ddns.net <<EOF
|
sshpass -p "demopassword123" sftp -P 2022 demo@sftpgo.momentry.ddns.net <<EOF
|
||||||
mkdir uploads
|
mkdir uploads
|
||||||
mkdir videos
|
mkdir videos
|
||||||
bye
|
bye
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# Momentry 系統測試與驗證計劃
|
# Momentry 系統測試與驗證計劃
|
||||||
|
|
||||||
|
| 項目 | 內容 |
|
||||||
|
|------|------|
|
||||||
|
| 建立者 | Warren |
|
||||||
|
| 建立時間 | 2026-03-23 |
|
||||||
|
| 文件版本 | V1.0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-23 | 創建測試與驗證計劃 | Warren | OpenCode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
> **計劃階段** - 僅供討論,尚未執行
|
> **計劃階段** - 僅供討論,尚未執行
|
||||||
> **建立時間**: 2026-03-23
|
> **建立時間**: 2026-03-23
|
||||||
> **目標**: 安裝後測試、跑分、燒機
|
> **目標**: 安裝後測試、跑分、燒機
|
||||||
|
|||||||
+15
-3
@@ -2,9 +2,21 @@
|
|||||||
|
|
||||||
| 項目 | 內容 |
|
| 項目 | 內容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 版本 | V1.0 |
|
| 建立者 | Warren |
|
||||||
| 日期 | 2026-03-21 |
|
| 建立時間 | 2026-03-21 |
|
||||||
| 目標讀者 | 系統管理員、開發者 |
|
| 文件版本 | V1.0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-21 | 創建使用手冊 | Warren | OpenCode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**目標讀者**: 系統管理員、開發者
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# Momentry Core 版本管理規範
|
# Momentry Core 版本管理規範
|
||||||
|
|
||||||
|
| 項目 | 內容 |
|
||||||
|
|------|------|
|
||||||
|
| 建立者 | Warren |
|
||||||
|
| 建立時間 | 2026-03-23 |
|
||||||
|
| 文件版本 | V1.0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-23 | 創建版本管理規範 | Warren | OpenCode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 1. 版本與通訊埠對照表
|
## 1. 版本與通訊埠對照表
|
||||||
|
|
||||||
| 版本 | Binary | Port | Redis Prefix | 用途 |
|
| 版本 | Binary | Port | Redis Prefix | 用途 |
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
# Video Registration
|
# Video Registration
|
||||||
|
|
||||||
|
| 項目 | 內容 |
|
||||||
|
|------|------|
|
||||||
|
| 建立者 | Warren |
|
||||||
|
| 建立時間 | 2026-03-25 |
|
||||||
|
| 文件版本 | V1.1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||||
|
|------|------|------|--------|-----------|
|
||||||
|
| V1.0 | 2026-03-25 | 創建文件 | Warren | OpenCode |
|
||||||
|
| V1.1 | 2026-03-26 | 修正 curl 範例,新增 API Key 驗證標頭 | OpenCode | deepseek-reasoner |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
影片註冊 API (`POST /api/v1/register`) 用於將影片加入 Momentry Core 系統進行處理。
|
影片註冊 API (`POST /api/v1/register`) 用於將影片加入 Momentry Core 系統進行處理。
|
||||||
@@ -139,11 +156,13 @@ SFTPgo 的用戶目錄結構:
|
|||||||
# 使用相對路徑註冊
|
# 使用相對路徑註冊
|
||||||
curl -X POST http://localhost:3002/api/v1/register \
|
curl -X POST http://localhost:3002/api/v1/register \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"path": "./demo/video.mp4"}'
|
-d '{"path": "./demo/video.mp4"}'
|
||||||
|
|
||||||
# 或使用多層目錄
|
# 或使用多層目錄
|
||||||
curl -X POST http://localhost:3002/api/v1/register \
|
curl -X POST http://localhost:3002/api/v1/register \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"path": "./demo/movies/2024/video.mp4"}'
|
-d '{"path": "./demo/movies/2024/video.mp4"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -185,6 +204,7 @@ pub fn extract_user_from_relative_path(relative_path: &str) -> (String, String)
|
|||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3002/api/v1/probe \
|
curl -X POST http://localhost:3002/api/v1/probe \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
-d '{"path": "./demo/video.mp4"}'
|
-d '{"path": "./demo/video.mp4"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -224,10 +244,3 @@ curl -X POST http://localhost:3002/api/v1/probe \
|
|||||||
| `src/core/probe/ffprobe.rs` | ffprobe 整合 |
|
| `src/core/probe/ffprobe.rs` | ffprobe 整合 |
|
||||||
| `docs/SFTPGO_DEMO_USER.md` | SFTPgo 用戶設置 |
|
| `docs/SFTPGO_DEMO_USER.md` | SFTPgo 用戶設置 |
|
||||||
| `docs/API_ENDPOINTS.md` | API 端點總覽 |
|
| `docs/API_ENDPOINTS.md` | API 端點總覽 |
|
||||||
|
|
||||||
## 歷史
|
|
||||||
|
|
||||||
| 日期 | 變更 |
|
|
||||||
|------|------|
|
|
||||||
| 2026-03-25 | 初始版本 - 新增 UUID 計算規則和重複註冊檢查 |
|
|
||||||
| 2026-03-25 | 新增 Probe API 說明 |
|
|
||||||
|
|||||||
@@ -12,7 +12,10 @@
|
|||||||
"name": "Webhook (Simple)",
|
"name": "Webhook (Simple)",
|
||||||
"type": "n8n-nodes-base.webhook",
|
"type": "n8n-nodes-base.webhook",
|
||||||
"typeVersion": 1,
|
"typeVersion": 1,
|
||||||
"position": [250, 300],
|
"position": [
|
||||||
|
250,
|
||||||
|
300
|
||||||
|
],
|
||||||
"webhookId": "video-search-simple"
|
"webhookId": "video-search-simple"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -34,7 +37,8 @@
|
|||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"headers": {
|
"headers": {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
|
"X-API-Key": "demo_api_key_12345"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -42,17 +46,23 @@
|
|||||||
"name": "搜尋 Momentry",
|
"name": "搜尋 Momentry",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 3,
|
"typeVersion": 3,
|
||||||
"position": [500, 300]
|
"position": [
|
||||||
|
500,
|
||||||
|
300
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "// 處理 Momentry 搜尋結果\nconst data = $input.first().json;\nconst hits = data.hits;\n\nif (!hits || hits.length === 0) {\n return {\n json: {\n success: false,\n message: '找不到相關結果',\n query: data.query\n }\n };\n}\n\n// 格式化結果\nconst formattedResults = hits.map((hit, idx) => ({\n index: idx + 1,\n id: hit.id,\n title: hit.title,\n text: hit.text,\n startTime: hit.start,\n endTime: hit.end,\n relevance: Math.round(hit.score * 100) + '%',\n videoUrl: hit.media_url,\n videoLink: hit.media_url + '#t=' + hit.start + ',' + hit.end\n}));\n\nreturn {\n json: {\n success: true,\n query: data.query,\n totalFound: data.count,\n results: formattedResults\n }\n};"
|
"jsCode": "// 處理 Momentry 搜尋結果\nconst data = $input.first().json;\nconst hits = data.hits;\n\nif (!hits || hits.length === 0) {\n return {\n json: {\n success: false,\n message: '找不到相關結果',\n query: data.query\n }\n };\n}\n\n// 格式化結果\nconst formattedResults = hits.map((hit, idx) => {\n return {\n index: idx + 1,\n id: hit.id,\n title: hit.title,\n text: hit.text,\n startTime: hit.start,\n endTime: hit.end,\n relevance: Math.round(hit.score * 100) + '%',\n file_path: hit.file_path\n };\n});\n\nreturn {\n json: {\n success: true,\n query: data.query,\n totalFound: data.count,\n results: formattedResults\n }\n};"
|
||||||
},
|
},
|
||||||
"id": "code-process-simple",
|
"id": "code-process-simple",
|
||||||
"name": "處理結果",
|
"name": "處理結果",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 1,
|
"typeVersion": 1,
|
||||||
"position": [750, 300]
|
"position": [
|
||||||
|
750,
|
||||||
|
300
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -63,7 +73,10 @@
|
|||||||
"name": "回傳結果",
|
"name": "回傳結果",
|
||||||
"type": "n8n-nodes-base.respondToWebhook",
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
"typeVersion": 1,
|
"typeVersion": 1,
|
||||||
"position": [1000, 300]
|
"position": [
|
||||||
|
1000,
|
||||||
|
300
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": {
|
"connections": {
|
||||||
@@ -107,4 +120,4 @@
|
|||||||
"versionId": "1",
|
"versionId": "1",
|
||||||
"createdAt": "2026-03-23T00:00:00.000Z",
|
"createdAt": "2026-03-23T00:00:00.000Z",
|
||||||
"updatedAt": "2026-03-23T00:00:00.000Z"
|
"updatedAt": "2026-03-23T00:00:00.000Z"
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,10 @@
|
|||||||
"name": "Webhook Trigger",
|
"name": "Webhook Trigger",
|
||||||
"type": "n8n-nodes-base.webhook",
|
"type": "n8n-nodes-base.webhook",
|
||||||
"typeVersion": 1,
|
"typeVersion": 1,
|
||||||
"position": [250, 300]
|
"position": [
|
||||||
|
250,
|
||||||
|
300
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -21,22 +24,31 @@
|
|||||||
"contentType": "json",
|
"contentType": "json",
|
||||||
"body": "={{ JSON.stringify({query: $json.body.query || $json.body, limit: $json.body.limit || 5, uuid: $json.body.uuid}) }}",
|
"body": "={{ JSON.stringify({query: $json.body.query || $json.body, limit: $json.body.limit || 5, uuid: $json.body.uuid}) }}",
|
||||||
"options": {
|
"options": {
|
||||||
"timeout": 30000
|
"timeout": 30000,
|
||||||
|
"headers": {
|
||||||
|
"X-API-Key": "demo_api_key_12345"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Search Momentry Core",
|
"name": "Search Momentry Core",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.1,
|
"typeVersion": 4.1,
|
||||||
"position": [500, 300]
|
"position": [
|
||||||
|
500,
|
||||||
|
300
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "// Process Momentry Core search results\nconst data = $input.first().json;\nconst hits = data.hits || [];\n\nif (hits.length === 0) {\n return {\n json: {\n success: false,\n message: 'No relevant results found',\n query: data.query,\n results: []\n }\n };\n}\n\n// Format results for RAG\nconst formattedResults = hits.map((hit, idx) => ({\n index: idx + 1,\n id: hit.id || hit.chunk_id,\n title: hit.title || 'Unknown Video',\n text: hit.text || hit.content || '',\n startTime: hit.start_time || hit.start || 0,\n endTime: hit.end_time || hit.end || 0,\n relevance: Math.round((hit.score || 0) * 100) + '%',\n videoUuid: hit.video_uuid || hit.uuid,\n mediaUrl: hit.media_url || '',\n deepLink: hit.media_url ? `${hit.media_url}#t=${hit.start_time || hit.start},${hit.end_time || hit.end}` : ''\n}));\n\n// Build context for RAG\nconst context = formattedResults\n .map(r => `[${r.index}] ${r.text} (Video: ${r.title}, Time: ${r.startTime}s-${r.endTime}s)`)\n .join('\\n\\n');\n\nreturn {\n json: {\n success: true,\n query: data.query,\n totalFound: data.count || hits.length,\n context: context,\n results: formattedResults\n }\n};"
|
"jsCode": "// Process Momentry Core search results\nconst data = $input.first().json;\nconst hits = data.hits || [];\n\nif (hits.length === 0) {\n return {\n json: {\n success: false,\n message: 'No relevant results found',\n query: data.query,\n results: []\n }\n };\n}\n\n// Format results for RAG\nconst formattedResults = hits.map((hit, idx) => {\n return {\n index: idx + 1,\n id: hit.id || hit.chunk_id,\n title: hit.title || 'Unknown Video',\n text: hit.text || hit.content || '',\n startTime: hit.start_time || hit.start || 0,\n endTime: hit.end_time || hit.end || 0,\n relevance: Math.round((hit.score || 0) * 100) + '%',\n videoUuid: hit.video_uuid || hit.uuid,\n file_path: hit.file_path || ''\n };\n});\n\n// Build context for RAG\nconst context = formattedResults\n .map(r => \\`[\\${r.index}] \\${r.text} (Video: \\${r.title}, Time: \\${r.startTime}s-\\${r.endTime}s)\\`)\n .join('\\n\\n');\n\nreturn {\n json: {\n success: true,\n query: data.query,\n totalFound: data.count || hits.length,\n context: context,\n results: formattedResults\n }\n};"
|
||||||
},
|
},
|
||||||
"name": "Process RAG Results",
|
"name": "Process RAG Results",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [750, 300]
|
"position": [
|
||||||
|
750,
|
||||||
|
300
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -49,7 +61,10 @@
|
|||||||
"name": "Respond to Webhook",
|
"name": "Respond to Webhook",
|
||||||
"type": "n8n-nodes-base.respondToWebhook",
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
"typeVersion": 1.5,
|
"typeVersion": 1.5,
|
||||||
"position": [1000, 300]
|
"position": [
|
||||||
|
1000,
|
||||||
|
300
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": {
|
"connections": {
|
||||||
@@ -91,4 +106,4 @@
|
|||||||
"executionOrder": "v1"
|
"executionOrder": "v1"
|
||||||
},
|
},
|
||||||
"staticData": null
|
"staticData": null
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,10 @@
|
|||||||
"name": "Webhook",
|
"name": "Webhook",
|
||||||
"type": "n8n-nodes-base.webhook",
|
"type": "n8n-nodes-base.webhook",
|
||||||
"typeVersion": 1,
|
"typeVersion": 1,
|
||||||
"position": [250, 300],
|
"position": [
|
||||||
|
250,
|
||||||
|
300
|
||||||
|
],
|
||||||
"webhookId": "video-search"
|
"webhookId": "video-search"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -34,7 +37,8 @@
|
|||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"headers": {
|
"headers": {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
|
"X-API-Key": "demo_api_key_12345"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -42,17 +46,23 @@
|
|||||||
"name": "搜尋 Momentry",
|
"name": "搜尋 Momentry",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 3,
|
"typeVersion": 3,
|
||||||
"position": [500, 300]
|
"position": [
|
||||||
|
500,
|
||||||
|
300
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "const hits = $input.first().json.hits;\n\nif (!hits || hits.length === 0) {\n return {\n json: {\n message: '找不到相關結果',\n query: $input.first().json.query\n }\n };\n}\n\nconst results = hits.map((hit, index) => ({\n number: index + 1,\n text: hit.text,\n start: hit.start,\n end: hit.end,\n score: Math.round(hit.score * 100) + '%',\n url: hit.media_url + '#t=' + hit.start + ',' + hit.end,\n video_title: hit.title\n}));\n\nreturn {\n json: {\n query: $input.first().json.query,\n count: $input.first().json.count,\n results: results\n }\n};"
|
"jsCode": "const hits = $input.first().json.hits;\n\nif (!hits || hits.length === 0) {\n return {\n json: {\n message: '找不到相關結果',\n query: $input.first().json.query\n }\n };\n}\n\nconst results = hits.map((hit, index) => {\n return {\n number: index + 1,\n text: hit.text,\n start: hit.start,\n end: hit.end,\n score: Math.round(hit.score * 100) + '%',\n video_title: hit.title,\n file_path: hit.file_path\n };\n});\n\nreturn {\n json: {\n query: $input.first().json.query,\n count: $input.first().json.count,\n results: results\n }\n};"
|
||||||
},
|
},
|
||||||
"id": "code-process",
|
"id": "code-process",
|
||||||
"name": "處理結果",
|
"name": "處理結果",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 1,
|
"typeVersion": 1,
|
||||||
"position": [750, 300]
|
"position": [
|
||||||
|
750,
|
||||||
|
300
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -77,7 +87,10 @@
|
|||||||
"name": "Telegram 通知",
|
"name": "Telegram 通知",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 3,
|
"typeVersion": 3,
|
||||||
"position": [1000, 300],
|
"position": [
|
||||||
|
1000,
|
||||||
|
300
|
||||||
|
],
|
||||||
"continueOnFail": true
|
"continueOnFail": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -122,4 +135,4 @@
|
|||||||
"versionId": "1",
|
"versionId": "1",
|
||||||
"createdAt": "2026-03-23T00:00:00.000Z",
|
"createdAt": "2026-03-23T00:00:00.000Z",
|
||||||
"updatedAt": "2026-03-23T00:00:00.000Z"
|
"updatedAt": "2026-03-23T00:00:00.000Z"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Installing Momentry Worker as a system service..."
|
||||||
|
|
||||||
|
# Copy worker plist to system LaunchDaemons
|
||||||
|
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.worker.plist /Library/LaunchDaemons/
|
||||||
|
|
||||||
|
# Load the service
|
||||||
|
sudo launchctl load /Library/LaunchDaemons/com.momentry.worker.plist
|
||||||
|
|
||||||
|
echo "Worker service installed successfully."
|
||||||
|
echo "Checking service status..."
|
||||||
|
launchctl list | grep com.momentry.worker || echo "Service not listed in user domain; check system domain."
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
-- ================================================================
|
||||||
|
-- Migration 004: Fix Processor Results Schema
|
||||||
|
-- Version: 004
|
||||||
|
-- Date: 2026-03-26
|
||||||
|
-- Description: Add missing output_data column and fix worker integration
|
||||||
|
-- ================================================================
|
||||||
|
|
||||||
|
-- 4.1.1: Add output_data column (JSONB) to processor_results
|
||||||
|
ALTER TABLE processor_results ADD COLUMN IF NOT EXISTS output_data JSONB;
|
||||||
|
|
||||||
|
-- 4.1.2: Update processor_results table - drop duration_secs column if exists (we'll compute it)
|
||||||
|
ALTER TABLE processor_results DROP COLUMN IF EXISTS duration_secs;
|
||||||
|
|
||||||
|
-- 4.1.3: Add computed duration column (stored as integer seconds)
|
||||||
|
ALTER TABLE processor_results ADD COLUMN IF NOT EXISTS duration_secs INT GENERATED ALWAYS AS (
|
||||||
|
CASE
|
||||||
|
WHEN completed_at IS NOT NULL AND started_at IS NOT NULL
|
||||||
|
THEN EXTRACT(EPOCH FROM (completed_at - started_at))::INT
|
||||||
|
ELSE NULL
|
||||||
|
END
|
||||||
|
) STORED;
|
||||||
|
|
||||||
|
-- 4.1.4: Add check constraint for processor values
|
||||||
|
ALTER TABLE processor_results DROP CONSTRAINT IF EXISTS chk_processor_results_processor;
|
||||||
|
ALTER TABLE processor_results ADD CONSTRAINT chk_processor_results_processor
|
||||||
|
CHECK (processor IN ('asr', 'cut', 'yolo', 'ocr', 'face', 'pose', 'asrx'));
|
||||||
|
|
||||||
|
-- 4.1.5: Create index on processor_results.output_data for JSON queries (optional)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_processor_results_output_data ON processor_results USING gin (output_data);
|
||||||
|
|
||||||
|
-- 4.1.6: Add foreign key from processor_results.video_id to videos.id if not exists
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.table_constraints
|
||||||
|
WHERE table_name = 'processor_results'
|
||||||
|
AND constraint_name = 'processor_results_video_id_fkey'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE processor_results ADD CONSTRAINT processor_results_video_id_fkey
|
||||||
|
FOREIGN KEY (video_id) REFERENCES videos(id) ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 4.1.7: Update monitor_jobs table - ensure processors column is correct type
|
||||||
|
ALTER TABLE monitor_jobs ALTER COLUMN processors TYPE VARCHAR(20)[] USING processors::VARCHAR(20)[];
|
||||||
|
ALTER TABLE monitor_jobs ALTER COLUMN completed_processors TYPE VARCHAR(20)[] USING completed_processors::VARCHAR(20)[];
|
||||||
|
ALTER TABLE monitor_jobs ALTER COLUMN failed_processors TYPE VARCHAR(20)[] USING failed_processors::VARCHAR(20)[];
|
||||||
|
|
||||||
|
-- 4.1.8: Add default values for arrays
|
||||||
|
ALTER TABLE monitor_jobs ALTER COLUMN processors SET DEFAULT '{"asr","cut","yolo","ocr","face","pose","asrx"}';
|
||||||
|
ALTER TABLE monitor_jobs ALTER COLUMN completed_processors SET DEFAULT '{}';
|
||||||
|
ALTER TABLE monitor_jobs ALTER COLUMN failed_processors SET DEFAULT '{}';
|
||||||
|
|
||||||
|
-- 4.1.9: Update existing rows to have default processor array
|
||||||
|
UPDATE monitor_jobs SET processors = '{"asr","cut","yolo","ocr","face","pose","asrx"}' WHERE processors IS NULL;
|
||||||
|
|
||||||
|
-- 4.1.10: Add index on monitor_jobs.processors for faster array operations
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_monitor_jobs_processors ON monitor_jobs USING gin (processors);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN processor_results.output_data IS 'JSON output from processor execution';
|
||||||
|
COMMENT ON COLUMN processor_results.duration_secs IS 'Computed duration in seconds (completed - started)';
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- ================================================================
|
||||||
|
-- Migration 005: Change duration_secs to FLOAT8
|
||||||
|
-- Version: 005
|
||||||
|
-- Date: 2026-03-26
|
||||||
|
-- Description: Change processor_results.duration_secs from INT to FLOAT8
|
||||||
|
-- to match Rust f64 type and preserve fractional seconds.
|
||||||
|
-- ================================================================
|
||||||
|
|
||||||
|
-- 5.1.1: Drop the existing generated column
|
||||||
|
ALTER TABLE processor_results DROP COLUMN IF EXISTS duration_secs;
|
||||||
|
|
||||||
|
-- 5.1.2: Re-add as double precision (float8) computed column
|
||||||
|
ALTER TABLE processor_results ADD COLUMN duration_secs DOUBLE PRECISION GENERATED ALWAYS AS (
|
||||||
|
CASE
|
||||||
|
WHEN completed_at IS NOT NULL AND started_at IS NOT NULL
|
||||||
|
THEN EXTRACT(EPOCH FROM (completed_at - started_at))
|
||||||
|
ELSE NULL
|
||||||
|
END
|
||||||
|
) STORED;
|
||||||
|
|
||||||
|
-- 5.1.3: Update comment
|
||||||
|
COMMENT ON COLUMN processor_results.duration_secs IS 'Computed duration in seconds (completed - started) as double precision';
|
||||||
@@ -30,6 +30,12 @@
|
|||||||
<key>DATABASE_URL</key>
|
<key>DATABASE_URL</key>
|
||||||
<string>postgres://accusys@localhost:5432/momentry</string>
|
<string>postgres://accusys@localhost:5432/momentry</string>
|
||||||
|
|
||||||
|
<key>DB_MAX_CONNECTIONS</key>
|
||||||
|
<string>50</string>
|
||||||
|
|
||||||
|
<key>DB_ACQUIRE_TIMEOUT</key>
|
||||||
|
<string>30</string>
|
||||||
|
|
||||||
<key>REDIS_URL</key>
|
<key>REDIS_URL</key>
|
||||||
<string>redis://:accusys@localhost:6379</string>
|
<string>redis://:accusys@localhost:6379</string>
|
||||||
|
|
||||||
@@ -40,7 +46,7 @@
|
|||||||
<string>http://localhost:11434</string>
|
<string>http://localhost:11434</string>
|
||||||
|
|
||||||
<key>QDRANT_URL</key>
|
<key>QDRANT_URL</key>
|
||||||
<string>http://localhost:6333</string>
|
<string>http://127.0.0.1:6333</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|
||||||
<key>RunAtLoad</key>
|
<key>RunAtLoad</key>
|
||||||
|
|||||||
+7
-1
@@ -30,6 +30,12 @@
|
|||||||
<key>DATABASE_URL</key>
|
<key>DATABASE_URL</key>
|
||||||
<string>postgres://accusys@localhost:5432/momentry</string>
|
<string>postgres://accusys@localhost:5432/momentry</string>
|
||||||
|
|
||||||
|
<key>DB_MAX_CONNECTIONS</key>
|
||||||
|
<string>50</string>
|
||||||
|
|
||||||
|
<key>DB_ACQUIRE_TIMEOUT</key>
|
||||||
|
<string>30</string>
|
||||||
|
|
||||||
<key>REDIS_URL</key>
|
<key>REDIS_URL</key>
|
||||||
<string>redis://:accusys@localhost:6379</string>
|
<string>redis://:accusys@localhost:6379</string>
|
||||||
|
|
||||||
@@ -40,7 +46,7 @@
|
|||||||
<string>http://localhost:11434</string>
|
<string>http://localhost:11434</string>
|
||||||
|
|
||||||
<key>QDRANT_URL</key>
|
<key>QDRANT_URL</key>
|
||||||
<string>http://localhost:6333</string>
|
<string>http://127.0.0.1:6333</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|
||||||
<key>RunAtLoad</key>
|
<key>RunAtLoad</key>
|
||||||
@@ -92,7 +92,7 @@ check_backup_status() {
|
|||||||
if [ -d "$service_backup_dir" ]; then
|
if [ -d "$service_backup_dir" ]; then
|
||||||
file_count=$(find "$service_backup_dir" -type f 2>/dev/null | wc -l)
|
file_count=$(find "$service_backup_dir" -type f 2>/dev/null | wc -l)
|
||||||
size=$(du -sb "$service_backup_dir" 2>/dev/null | cut -f1)
|
size=$(du -sb "$service_backup_dir" 2>/dev/null | cut -f1)
|
||||||
latest_file=$(find "$service_backup_dir" -type f \( -name "*.tar.gz" -o -name "*.sql.gz" -o -name "*.rdb" \) 2>/dev/null | head -1)
|
latest_file=$(find "$service_backup_dir" -type f \( -name "*.tar.gz" -o -name "*.sql.gz" -o -name "*.rdb" \) -printf "%T@ %p\n" 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)
|
||||||
|
|
||||||
# 處理 size 為空或 0 的情況
|
# 處理 size 為空或 0 的情況
|
||||||
if [ -z "$size" ] || [ "$size" = "0" ]; then
|
if [ -z "$size" ] || [ "$size" = "0" ]; then
|
||||||
@@ -271,12 +271,12 @@ tier_backups() {
|
|||||||
|
|
||||||
# 7天前: daily -> weekly
|
# 7天前: daily -> weekly
|
||||||
# 命名格式: {service}_{type}_{YYYYMMDD}_{HHMMSS}.{ext}
|
# 命名格式: {service}_{type}_{YYYYMMDD}_{HHMMSS}.{ext}
|
||||||
find "$BACKUP_BASE/daily" -type f -mtime +7 | while read -r file; do
|
find "$BACKUP_BASE/daily" -type f -mtime +6 | while read -r file; do
|
||||||
service=$(basename "$(dirname "$file")")
|
service=$(basename "$(dirname "$file")")
|
||||||
|
|
||||||
# 解析時間戳
|
# 解析時間戳
|
||||||
filename=$(basename "$file")
|
filename=$(basename "$file")
|
||||||
timestamp=$(echo "$filename" | grep -oP '\d{8}_\d{6}' || echo "")
|
timestamp=$(echo "$filename" | grep -oE '[0-9]{8}_[0-9]{6}' || echo "")
|
||||||
|
|
||||||
if [ -n "$timestamp" ]; then
|
if [ -n "$timestamp" ]; then
|
||||||
year=${timestamp:0:4}
|
year=${timestamp:0:4}
|
||||||
|
|||||||
Binary file not shown.
Regular → Executable
+54
@@ -3,17 +3,65 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
from faster_whisper import WhisperModel
|
from faster_whisper import WhisperModel
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
from redis_publisher import RedisPublisher
|
from redis_publisher import RedisPublisher
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
print(f"ASR: Received signal {signum}, exiting...")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def has_audio_stream(video_path):
|
||||||
|
"""Check if video file has audio stream using ffprobe."""
|
||||||
|
try:
|
||||||
|
cmd = [
|
||||||
|
"ffprobe",
|
||||||
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-select_streams",
|
||||||
|
"a",
|
||||||
|
"-show_entries",
|
||||||
|
"stream=codec_type",
|
||||||
|
"-of",
|
||||||
|
"csv=p=0",
|
||||||
|
video_path,
|
||||||
|
]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||||
|
return bool(result.stdout.strip())
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return False
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("WARNING: ffprobe not found, assuming audio exists")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def run_asr(video_path, output_path, uuid: str = ""):
|
def run_asr(video_path, output_path, uuid: str = ""):
|
||||||
|
# Set up signal handlers
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
publisher = RedisPublisher(uuid) if uuid else None
|
publisher = RedisPublisher(uuid) if uuid else None
|
||||||
if publisher:
|
if publisher:
|
||||||
publisher.info("asr", "ASR_START")
|
publisher.info("asr", "ASR_START")
|
||||||
|
|
||||||
|
# Check for audio stream
|
||||||
|
if not has_audio_stream(video_path):
|
||||||
|
if publisher:
|
||||||
|
publisher.info("asr", "No audio stream detected, skipping transcription")
|
||||||
|
output = {"language": "", "language_probability": 0.0, "segments": []}
|
||||||
|
with open(output_path, "w") as f:
|
||||||
|
json.dump(output, f, indent=2)
|
||||||
|
if publisher:
|
||||||
|
publisher.complete("asr", "0 segments (no audio)")
|
||||||
|
sys.stderr.write("ASR: No audio stream, skipping transcription\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
if publisher:
|
if publisher:
|
||||||
publisher.info("asr", "Loading Whisper model...")
|
publisher.info("asr", "Loading Whisper model...")
|
||||||
|
|
||||||
@@ -53,6 +101,12 @@ def run_asr(video_path, output_path, uuid: str = ""):
|
|||||||
if publisher:
|
if publisher:
|
||||||
publisher.complete("asr", f"{len(results)} segments")
|
publisher.complete("asr", f"{len(results)} segments")
|
||||||
|
|
||||||
|
sys.stderr.write(
|
||||||
|
f"ASR: Transcription complete, {len(results)} segments written to {output_path}\n"
|
||||||
|
)
|
||||||
|
sys.stderr.flush()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="ASR Transcription")
|
parser = argparse.ArgumentParser(description="ASR Transcription")
|
||||||
|
|||||||
Executable
+821
@@ -0,0 +1,821 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
export PATH="/usr/local/bin:/opt/homebrew/bin:/opt/homebrew/opt/postgresql@18/bin:/usr/bin:/bin:/sbin:/opt/homebrew/opt/mysql-client/bin:$PATH"
|
||||||
|
|
||||||
|
#===============================================================================
|
||||||
|
# Momentry 統一備份腳本
|
||||||
|
# 路徑: /Users/accusys/momentry/scripts/backup_all.sh
|
||||||
|
#
|
||||||
|
# 命名規範 (v2):
|
||||||
|
# {service}_{type}_v2_{YYYYMMDD}_{HHMMSS}.{ext}
|
||||||
|
#
|
||||||
|
# 版本說明:
|
||||||
|
# v1: 初始備份架構(不包含新架構組件)
|
||||||
|
# v2: 新架構備份(包含 monitor_jobs, processor_results, Output 目錄)
|
||||||
|
#
|
||||||
|
# 使用方式:
|
||||||
|
# ./backup_all.sh [service|all] [type] [timestamp]
|
||||||
|
#
|
||||||
|
# 參數:
|
||||||
|
# service - 特定服務 (postgresql, redis, mariadb, wordpress, n8n, qdrant, gitea, ollama, caddy, sftpgo, mongodb, php, momentry_output)
|
||||||
|
# all - 備份所有服務 (默認)
|
||||||
|
# type - 備份類型 (full, db, cfg, data)
|
||||||
|
# timestamp - 指定時間戳 (格式: YYYYMMDD_HHMMSS)
|
||||||
|
#
|
||||||
|
# 示例:
|
||||||
|
# ./backup_all.sh # 備份所有服務 (v2)
|
||||||
|
# ./backup_all.sh postgresql # 只備份 PostgreSQL
|
||||||
|
# ./backup_all.sh all full # 完整備份所有服務 (v2)
|
||||||
|
# ./backup_all.sh mariadb db # 只備份 MariaDB 數據庫
|
||||||
|
# ./backup_all.sh restore 20260316_101215 # 恢復到指定斷點
|
||||||
|
#
|
||||||
|
# ⚠️ v2 版本差異:
|
||||||
|
# - 新增 monitor_jobs, processor_results 表
|
||||||
|
# - 新增 Output 目錄備份
|
||||||
|
# - MongoDB 路徑修正
|
||||||
|
#
|
||||||
|
# 排程範例 (crontab):
|
||||||
|
# # 每天凌晨 3 點執行所有備份
|
||||||
|
# 0 3 * * * /Users/accusys/momentry/scripts/backup_all.sh >> /Users/accusys/momentry/log/backup.log 2>&1
|
||||||
|
#
|
||||||
|
# # 每週日凌晨 3 點執行完整備份
|
||||||
|
# 0 3 * * 0 /Users/accusys/momentry/scripts/backup_all.sh all full >> /Users/accusys/momentry/log/backup.log 2>&1
|
||||||
|
#===============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 載入密碼配置
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
if [ -f "$SCRIPT_DIR/load_credentials.sh" ]; then
|
||||||
|
source "$SCRIPT_DIR/load_credentials.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 確保路徑正確(Crontab 環境可能缺少 PATH)
|
||||||
|
export PATH="/usr/local/bin:/opt/homebrew/bin:/opt/homebrew/opt/postgresql@18/bin:/sbin:/usr/sbin:/usr/bin:/bin:/opt/homebrew/opt/mysql-client/bin"
|
||||||
|
|
||||||
|
# 顏色定義
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# 路徑配置
|
||||||
|
BACKUP_ROOT="/Users/accusys/momentry/backup/daily"
|
||||||
|
LOG_DIR="/Users/accusys/momentry/log"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# 備份版本 (v2 = 新架構)
|
||||||
|
BACKUP_VERSION="v2"
|
||||||
|
|
||||||
|
# 時間戳 (v2 格式: v2_YYYYMMDD_HHMMSS)
|
||||||
|
if [ -n "$3" ]; then
|
||||||
|
TIMESTAMP="$3"
|
||||||
|
else
|
||||||
|
TIMESTAMP="${BACKUP_VERSION}_$(date +%Y%m%d_%H%M%S)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 服務列表 (v2 新增 momentry_output)
|
||||||
|
SERVICES=("postgresql" "redis" "mariadb" "wordpress" "n8n" "qdrant" "gitea" "ollama" "caddy" "sftpgo" "mongodb" "php" "momentry_output")
|
||||||
|
|
||||||
|
#===============================================================================
|
||||||
|
# 日誌函數
|
||||||
|
#===============================================================================
|
||||||
|
log() {
|
||||||
|
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_DIR/backup.log"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] ✅ $1${NC}" | tee -a "$LOG_DIR/backup.log"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] ❌ $1${NC}" | tee -a "$LOG_DIR/backup.log"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ $1${NC}" | tee -a "$LOG_DIR/backup.log"
|
||||||
|
}
|
||||||
|
|
||||||
|
#===============================================================================
|
||||||
|
# 通用函數
|
||||||
|
#===============================================================================
|
||||||
|
ensure_backup_dir() {
|
||||||
|
local service=$1
|
||||||
|
mkdir -p "$BACKUP_ROOT/$service"
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_file() {
|
||||||
|
local service=$1
|
||||||
|
local type=$2
|
||||||
|
local file=$3
|
||||||
|
|
||||||
|
ensure_backup_dir "$service"
|
||||||
|
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
local filename=$(basename "$file")
|
||||||
|
local dest="$BACKUP_ROOT/$service/${service}_${type}_${TIMESTAMP}_${filename}"
|
||||||
|
cp "$file" "$dest"
|
||||||
|
|
||||||
|
# 壓縮
|
||||||
|
if [[ "$filename" == *.sql ]]; then
|
||||||
|
gzip "$dest"
|
||||||
|
dest="${dest}.gz"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SHA256
|
||||||
|
sha256sum "$dest" >"${dest}.sha256"
|
||||||
|
|
||||||
|
log_success "$service $type: $(basename "$dest")"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_directory() {
|
||||||
|
local service=$1
|
||||||
|
local type=$2
|
||||||
|
local dir=$3
|
||||||
|
|
||||||
|
ensure_backup_dir "$service"
|
||||||
|
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
local dest="$BACKUP_ROOT/$service/${service}_${type}_${TIMESTAMP}.tar.gz"
|
||||||
|
tar -czf "$dest" -C "$(dirname "$dir")" "$(basename "$dir")" 2>/dev/null || true
|
||||||
|
|
||||||
|
# SHA256
|
||||||
|
sha256sum "$dest" >"${dest}.sha256"
|
||||||
|
|
||||||
|
log_success "$service $type: $(basename "$dest")"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#===============================================================================
|
||||||
|
# 服務備份函數
|
||||||
|
#===============================================================================
|
||||||
|
|
||||||
|
# PostgreSQL
|
||||||
|
backup_postgresql() {
|
||||||
|
local type=${1:-db}
|
||||||
|
log "開始 PostgreSQL 備份..."
|
||||||
|
|
||||||
|
# momentry 數據庫
|
||||||
|
PGPASSWORD="$PG_PASSWORD" pg_dump -U "$PG_USER" -d momentry | gzip >"$BACKUP_ROOT/postgresql/postgresql_db_momentry_${TIMESTAMP}.sql.gz"
|
||||||
|
sha256sum "$BACKUP_ROOT/postgresql/postgresql_db_momentry_${TIMESTAMP}.sql.gz" >"$BACKUP_ROOT/postgresql/postgresql_db_${TIMESTAMP}.sha256"
|
||||||
|
|
||||||
|
# video_register 數據庫
|
||||||
|
PGPASSWORD="$PG_PASSWORD" pg_dump -U "$PG_USER" -d video_register | gzip >"$BACKUP_ROOT/postgresql/postgresql_db_video_register_${TIMESTAMP}.sql.gz"
|
||||||
|
sha256sum "$BACKUP_ROOT/postgresql/postgresql_db_video_register_${TIMESTAMP}.sql.gz" >>"$BACKUP_ROOT/postgresql/postgresql_db_${TIMESTAMP}.sha256"
|
||||||
|
|
||||||
|
log_success "PostgreSQL: 數據庫備份完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
backup_redis() {
|
||||||
|
local type=${1:-rdb}
|
||||||
|
log "開始 Redis 備份..."
|
||||||
|
|
||||||
|
redis-cli -a "$REDIS_PASSWORD" SAVE >/dev/null 2>&1
|
||||||
|
cp /opt/homebrew/var/db/redis/dump.rdb "$BACKUP_ROOT/redis/redis_rdb_${TIMESTAMP}.rdb"
|
||||||
|
sha256sum "$BACKUP_ROOT/redis/redis_rdb_${TIMESTAMP}.rdb" >"$BACKUP_ROOT/redis/redis_rdb_${TIMESTAMP}.sha256"
|
||||||
|
|
||||||
|
log_success "Redis: RDB 備份完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# MariaDB (包含 WordPress)
|
||||||
|
backup_mariadb() {
|
||||||
|
local type=${1:-db}
|
||||||
|
log "開始 MariaDB 備份..."
|
||||||
|
|
||||||
|
# 所有數據庫
|
||||||
|
mysqldump -u "$MARIADB_USER" -p"$MARIADB_PASSWORD" --all-databases | gzip > \
|
||||||
|
"$BACKUP_ROOT/mariadb/mariadb_db_all_${TIMESTAMP}.sql.gz"
|
||||||
|
sha256sum "$BACKUP_ROOT/mariadb/mariadb_db_all_${TIMESTAMP}.sql.gz" >"$BACKUP_ROOT/mariadb/mariadb_db_${TIMESTAMP}.sha256"
|
||||||
|
|
||||||
|
# WordPress 數據庫
|
||||||
|
mysqldump -u "$MARIADB_USER" -p"$MARIADB_PASSWORD" wordpress | gzip > \
|
||||||
|
"$BACKUP_ROOT/mariadb/mariadb_db_wordpress_${TIMESTAMP}.sql.gz"
|
||||||
|
sha256sum "$BACKUP_ROOT/mariadb/mariadb_db_wordpress_${TIMESTAMP}.sql.gz" >>"$BACKUP_ROOT/mariadb/mariadb_db_${TIMESTAMP}.sha256"
|
||||||
|
|
||||||
|
log_success "MariaDB: 數據庫備份完成 (包含 WordPress)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# WordPress 文件
|
||||||
|
backup_wordpress_files() {
|
||||||
|
local wordpress_dir="/Users/accusys/wordpress/web"
|
||||||
|
local backup_dir="$BACKUP_ROOT/wordpress"
|
||||||
|
|
||||||
|
log "開始 WordPress 文件備份..."
|
||||||
|
|
||||||
|
# 確保備份目錄存在
|
||||||
|
mkdir -p "$backup_dir"
|
||||||
|
|
||||||
|
# 排除不必要的目錄
|
||||||
|
if [ -d "$wordpress_dir" ]; then
|
||||||
|
tar --exclude='wp-content/cache/*' \
|
||||||
|
--exclude='wp-content/uploads/cache/*' \
|
||||||
|
--exclude='.git/*' \
|
||||||
|
-czf "$backup_dir/wordpress_files_${TIMESTAMP}.tar.gz" \
|
||||||
|
-C /Users/accusys/wordpress web/
|
||||||
|
|
||||||
|
sha256sum "$backup_dir/wordpress_files_${TIMESTAMP}.tar.gz" >>"$backup_dir/wordpress_${TIMESTAMP}.sha256" 2>/dev/null ||
|
||||||
|
sha256sum "$backup_dir/wordpress_files_${TIMESTAMP}.tar.gz" >"$backup_dir/wordpress_${TIMESTAMP}.sha256"
|
||||||
|
|
||||||
|
log_success "WordPress: 文件備份完成"
|
||||||
|
else
|
||||||
|
log_error "WordPress 目錄不存在: $wordpress_dir"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# n8n
|
||||||
|
backup_n8n() {
|
||||||
|
local type=${1:-full}
|
||||||
|
log "開始 n8n 備份..."
|
||||||
|
|
||||||
|
# 數據庫
|
||||||
|
PGPASSWORD="$PG_PASSWORD" pg_dump -U "$PG_USER" -d n8n | gzip >"$BACKUP_ROOT/n8n/n8n_db_${TIMESTAMP}.sql.gz"
|
||||||
|
|
||||||
|
# 數據目錄
|
||||||
|
if [ -d "/Users/accusys/momentry/var/n8n" ]; then
|
||||||
|
tar -czf "$BACKUP_ROOT/n8n/n8n_data_${TIMESTAMP}.tar.gz" -C /Users/accusys/momentry/var n8n/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SHA256
|
||||||
|
sha256sum "$BACKUP_ROOT/n8n"/n8n_* >"$BACKUP_ROOT/n8n/n8n_${TIMESTAMP}.sha256"
|
||||||
|
|
||||||
|
log_success "n8n: 完整備份完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Qdrant
|
||||||
|
backup_qdrant() {
|
||||||
|
local type=${1:-full}
|
||||||
|
log "開始 Qdrant 備份..."
|
||||||
|
|
||||||
|
# 嘗試使用 Snapshots API
|
||||||
|
COLLECTIONS=$(curl -s -H "api-key: $QDRANT_API_KEY" \
|
||||||
|
http://localhost:6333/collections | jq -r '.result[].name' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -n "$COLLECTIONS" ] && [ "$COLLECTIONS" != "null" ]; then
|
||||||
|
for COLLECTION in $COLLECTIONS; do
|
||||||
|
curl -X POST -H "api-key: $QDRANT_API_KEY" \
|
||||||
|
"http://localhost:6333/collections/${COLLECTION}/snapshots" \
|
||||||
|
-o "$BACKUP_ROOT/qdrant/qdrant_snapshot_${COLLECTION}_${TIMESTAMP}.tar.gz" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
else
|
||||||
|
# 數據目錄備份
|
||||||
|
tar -czf "$BACKUP_ROOT/qdrant/qdrant_data_${TIMESTAMP}.tar.gz" \
|
||||||
|
-C /Users/accusys/momentry/var qdrant/ 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SHA256
|
||||||
|
sha256sum "$BACKUP_ROOT/qdrant"/qdrant_* >"$BACKUP_ROOT/qdrant/qdrant_${TIMESTAMP}.sha256"
|
||||||
|
|
||||||
|
log_success "Qdrant: 備份完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gitea
|
||||||
|
backup_gitea() {
|
||||||
|
local type=${1:-full}
|
||||||
|
log "開始 Gitea 備份..."
|
||||||
|
|
||||||
|
# 數據目錄
|
||||||
|
if [ -d "/Users/accusys/momentry/var/gitea" ]; then
|
||||||
|
tar -czf "$BACKUP_ROOT/gitea/gitea_data_${TIMESTAMP}.tar.gz" \
|
||||||
|
-C /Users/accusys/momentry/var gitea/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 配置目錄
|
||||||
|
if [ -d "/Users/accusys/momentry/etc/gitea" ]; then
|
||||||
|
tar -czf "$BACKUP_ROOT/gitea/gitea_cfg_${TIMESTAMP}.tar.gz" \
|
||||||
|
-C /Users/accusys/momentry/etc gitea/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SHA256
|
||||||
|
sha256sum "$BACKUP_ROOT/gitea"/gitea_* >"$BACKUP_ROOT/gitea/gitea_${TIMESTAMP}.sha256"
|
||||||
|
|
||||||
|
log_success "Gitea: 完整備份完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ollama
|
||||||
|
backup_ollama() {
|
||||||
|
local type=${1:-cfg}
|
||||||
|
log "開始 Ollama 備份..."
|
||||||
|
|
||||||
|
# 配置目錄
|
||||||
|
if [ -d "/Users/accusys/momentry/etc/ollama" ]; then
|
||||||
|
tar -czf "$BACKUP_ROOT/ollama/ollama_cfg_${TIMESTAMP}.tar.gz" \
|
||||||
|
-C /Users/accusys/momentry/etc ollama/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 環境變數
|
||||||
|
if [ -f "/Users/accusys/momentry/var/ollama/environment.txt" ]; then
|
||||||
|
cp /Users/accusys/momentry/var/ollama/environment.txt "$BACKUP_ROOT/ollama/ollama_env_${TIMESTAMP}.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SHA256
|
||||||
|
sha256sum "$BACKUP_ROOT/ollama"/ollama_* >"$BACKUP_ROOT/ollama/ollama_${TIMESTAMP}.sha256"
|
||||||
|
|
||||||
|
log_success "Ollama: 配置備份完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Caddy
|
||||||
|
backup_caddy() {
|
||||||
|
local type=${1:-cfg}
|
||||||
|
log "開始 Caddy 備份..."
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
if [ -f "/Users/accusys/momentry/etc/Caddyfile" ]; then
|
||||||
|
tar -czf "$BACKUP_ROOT/caddy/caddy_cfg_${TIMESTAMP}.tar.gz" \
|
||||||
|
-C /Users/accusys/momentry/etc Caddyfile
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SHA256
|
||||||
|
sha256sum "$BACKUP_ROOT/caddy"/caddy_* >"$BACKUP_ROOT/caddy/caddy_${TIMESTAMP}.sha256"
|
||||||
|
|
||||||
|
log_success "Caddy: 配置備份完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# SftpGo
|
||||||
|
backup_sftpgo() {
|
||||||
|
local type=${1:-cfg}
|
||||||
|
log "開始 SftpGo 備份..."
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
if [ -d "/Users/accusys/momentry/etc/sftpgo" ]; then
|
||||||
|
tar -czf "$BACKUP_ROOT/sftpgo/sftpgo_cfg_${TIMESTAMP}.tar.gz" \
|
||||||
|
-C /Users/accusys/momentry/etc sftpgo/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PostgreSQL 數據庫 (SFTPGo 已遷移到 PostgreSQL)
|
||||||
|
PGPASSWORD="$SFTPGO_PASSWORD" pg_dump -U "$SFTPGO_USER" -h localhost -d sftpgo | gzip >"$BACKUP_ROOT/sftpgo/sftpgo_db_${TIMESTAMP}.sql.gz"
|
||||||
|
|
||||||
|
# SHA256
|
||||||
|
sha256sum "$BACKUP_ROOT/sftpgo"/sftpgo_* >"$BACKUP_ROOT/sftpgo/sftpgo_${TIMESTAMP}.sha256"
|
||||||
|
|
||||||
|
log_success "SftpGo: 配置和數據庫備份完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# MongoDB
|
||||||
|
backup_mongodb() {
|
||||||
|
local type=${1:-full}
|
||||||
|
log "開始 MongoDB 備份..."
|
||||||
|
|
||||||
|
# 使用 mongodump 備份 (避免文件鎖問題)
|
||||||
|
local MONGO_BACKUP_DIR="/tmp/mongodb_backup_${TIMESTAMP}"
|
||||||
|
mkdir -p "$MONGO_BACKUP_DIR"
|
||||||
|
|
||||||
|
# mongodump 需要認證
|
||||||
|
if [ -n "$MONGODB_PASSWORD" ]; then
|
||||||
|
mongodump --uri="mongodb://localhost:27017" \
|
||||||
|
--username="$MONGODB_USER" \
|
||||||
|
--password="$MONGODB_PASSWORD" \
|
||||||
|
--authenticationDatabase=admin \
|
||||||
|
--out="$MONGO_BACKUP_DIR" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
mongodump --uri="mongodb://localhost:27017" \
|
||||||
|
--out="$MONGO_BACKUP_DIR" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 打包
|
||||||
|
if [ -d "$MONGO_BACKUP_DIR" ] && [ "$(ls -A $MONGO_BACKUP_DIR 2>/dev/null)" ]; then
|
||||||
|
tar -czf "$BACKUP_ROOT/mongodb/mongodb_data_${TIMESTAMP}.tar.gz" \
|
||||||
|
-C "$MONGO_BACKUP_DIR" .
|
||||||
|
rm -rf "$MONGO_BACKUP_DIR"
|
||||||
|
log "MongoDB: mongodump 備份完成"
|
||||||
|
else
|
||||||
|
log_warn "MongoDB: mongodump 備份失敗或數據庫為空"
|
||||||
|
rm -rf "$MONGO_BACKUP_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SHA256
|
||||||
|
sha256sum "$BACKUP_ROOT/mongodb"/mongodb_* >"$BACKUP_ROOT/mongodb/mongodb_${TIMESTAMP}.sha256"
|
||||||
|
|
||||||
|
log_success "MongoDB: 備份完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# PHP
|
||||||
|
backup_php() {
|
||||||
|
local type=${1:-cfg}
|
||||||
|
log "開始 PHP 備份..."
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
if [ -d "/Users/accusys/momentry/etc/php/8.5" ]; then
|
||||||
|
tar -czf "$BACKUP_ROOT/php/php_cfg_${TIMESTAMP}.tar.gz" \
|
||||||
|
-C /Users/accusys/momentry/etc php/8.5
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SHA256
|
||||||
|
sha256sum "$BACKUP_ROOT/php"/php_* >"$BACKUP_ROOT/php/php_${TIMESTAMP}.sha256"
|
||||||
|
|
||||||
|
log_success "PHP: 配置備份完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Momentry Output 目錄 (v2 新增)
|
||||||
|
backup_momentry_output() {
|
||||||
|
local type=${1:-data}
|
||||||
|
log "開始 Momentry Output 備份..."
|
||||||
|
|
||||||
|
# Output 目錄
|
||||||
|
local OUTPUT_DIR="/Users/accusys/momentry/output"
|
||||||
|
|
||||||
|
if [ -d "$OUTPUT_DIR" ]; then
|
||||||
|
tar -czf "$BACKUP_ROOT/momentry/momentry_output_${TIMESTAMP}.tar.gz" \
|
||||||
|
-C /Users/accusys/momentry output/
|
||||||
|
log "Momentry Output: 備份 $OUTPUT_DIR"
|
||||||
|
else
|
||||||
|
log_warn "Momentry Output: 目錄不存在或為空 ($OUTPUT_DIR)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SHA256
|
||||||
|
sha256sum "$BACKUP_ROOT/momentry"/momentry_output_* >"$BACKUP_ROOT/momentry/momentry_output_${TIMESTAMP}.sha256" 2>/dev/null || true
|
||||||
|
|
||||||
|
log_success "Momentry Output: 備份完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
#===============================================================================
|
||||||
|
# 恢復函數
|
||||||
|
#===============================================================================
|
||||||
|
|
||||||
|
restore_postgresql() {
|
||||||
|
local timestamp=$1
|
||||||
|
log "恢復 PostgreSQL..."
|
||||||
|
|
||||||
|
# 找到對應的備份文件
|
||||||
|
local backup_file=$(ls "$BACKUP_ROOT/postgresql"/postgresql_db_momentry_${timestamp}.sql.gz 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -n "$backup_file" ]; then
|
||||||
|
gunzip -c "$backup_file" | PGPASSWORD="$PG_PASSWORD" psql -U "$PG_USER" -d momentry
|
||||||
|
log_success "PostgreSQL 恢復完成"
|
||||||
|
else
|
||||||
|
log_error "找不到 PostgreSQL 備份文件: $timestamp"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_redis() {
|
||||||
|
local timestamp=$1
|
||||||
|
log "恢復 Redis..."
|
||||||
|
|
||||||
|
local backup_file=$(ls "$BACKUP_ROOT/redis"/redis_rdb_${timestamp}.rdb 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -n "$backup_file" ]; then
|
||||||
|
redis-cli -a "$REDIS_PASSWORD" SHUTDOWN 2>/dev/null || true
|
||||||
|
cp "$backup_file" /opt/homebrew/var/db/redis/dump.rdb
|
||||||
|
launchctl load /Library/LaunchDaemons/com.momentry.redis.plist 2>/dev/null ||
|
||||||
|
redis-server --daemonize yes --requirepass "$REDIS_PASSWORD"
|
||||||
|
log_success "Redis 恢復完成"
|
||||||
|
else
|
||||||
|
log_error "找不到 Redis 備份文件: $timestamp"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_mariadb() {
|
||||||
|
local timestamp=$1
|
||||||
|
log "恢復 MariaDB (包含 WordPress)..."
|
||||||
|
|
||||||
|
local backup_file=$(ls "$BACKUP_ROOT/mariadb"/mariadb_db_wordpress_${timestamp}.sql.gz 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -n "$backup_file" ]; then
|
||||||
|
gunzip -c "$backup_file" | mysql -u momentry_backup -pmomentry_backup_pwd_2026 wordpress
|
||||||
|
log_success "MariaDB/WordPress 恢復完成"
|
||||||
|
else
|
||||||
|
log_error "找不到 MariaDB 備份文件: $timestamp"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_n8n() {
|
||||||
|
local timestamp=$1
|
||||||
|
log "恢復 n8n..."
|
||||||
|
|
||||||
|
# 恢復數據庫
|
||||||
|
local db_backup=$(ls "$BACKUP_ROOT/n8n"/n8n_db_${timestamp}.sql.gz 2>/dev/null | head -1)
|
||||||
|
if [ -n "$db_backup" ]; then
|
||||||
|
gunzip -c "$db_backup" | PGPASSWORD="$PG_PASSWORD" psql -U "$PG_USER" -d n8n
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 恢復數據目錄
|
||||||
|
local data_backup=$(ls "$BACKUP_ROOT/n8n"/n8n_data_${timestamp}.tar.gz 2>/dev/null | head -1)
|
||||||
|
if [ -n "$data_backup" ]; then
|
||||||
|
rm -rf /Users/accusys/momentry/var/n8n
|
||||||
|
tar -xzf "$data_backup" -C /Users/accusys/momentry/var/
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "n8n 恢復完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_qdrant() {
|
||||||
|
local timestamp=$1
|
||||||
|
log "恢復 Qdrant..."
|
||||||
|
|
||||||
|
pkill qdrant 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
local data_backup=$(ls "$BACKUP_ROOT/qdrant"/qdrant_data_${timestamp}.tar.gz 2>/dev/null | head -1)
|
||||||
|
if [ -n "$data_backup" ]; then
|
||||||
|
rm -rf /Users/accusys/momentry/var/qdrant
|
||||||
|
tar -xzf "$data_backup" -C /Users/accusys/momentry/var/
|
||||||
|
fi
|
||||||
|
|
||||||
|
launchctl load /Library/LaunchDaemons/com.momentry.qdrant.plist 2>/dev/null || true
|
||||||
|
log_success "Qdrant 恢復完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_gitea() {
|
||||||
|
local timestamp=$1
|
||||||
|
log "恢復 Gitea..."
|
||||||
|
|
||||||
|
# 停止 Gitea
|
||||||
|
pkill gitea 2>/dev/null || true
|
||||||
|
|
||||||
|
# 恢復數據
|
||||||
|
local data_backup=$(ls "$BACKUP_ROOT/gitea"/gitea_data_${timestamp}.tar.gz 2>/dev/null | head -1)
|
||||||
|
if [ -n "$data_backup" ]; then
|
||||||
|
rm -rf /Users/accusys/momentry/var/gitea
|
||||||
|
tar -xzf "$data_backup" -C /Users/accusys/momentry/var/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 恢復配置
|
||||||
|
local cfg_backup=$(ls "$BACKUP_ROOT/gitea"/gitea_cfg_${timestamp}.tar.gz 2>/dev/null | head -1)
|
||||||
|
if [ -n "$cfg_backup" ]; then
|
||||||
|
rm -rf /Users/accusys/momentry/etc/gitea
|
||||||
|
tar -xzf "$cfg_backup" -C /Users/accusys/momentry/etc/
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Gitea 恢復完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_ollama() {
|
||||||
|
local timestamp=$1
|
||||||
|
log "恢復 Ollama..."
|
||||||
|
|
||||||
|
# 恢復配置
|
||||||
|
local cfg_backup=$(ls "$BACKUP_ROOT/ollama"/ollama_cfg_${timestamp}.tar.gz 2>/dev/null | head -1)
|
||||||
|
if [ -n "$cfg_backup" ]; then
|
||||||
|
rm -rf /Users/accusys/momentry/etc/ollama
|
||||||
|
tar -xzf "$cfg_backup" -C /Users/accusys/momentry/etc/
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Ollama 恢復完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_caddy() {
|
||||||
|
local timestamp=$1
|
||||||
|
log "恢復 Caddy..."
|
||||||
|
|
||||||
|
local cfg_backup=$(ls "$BACKUP_ROOT/caddy"/caddy_cfg_${timestamp}.tar.gz 2>/dev/null | head -1)
|
||||||
|
if [ -n "$cfg_backup" ]; then
|
||||||
|
tar -xzf "$cfg_backup" -C /Users/accusys/momentry/etc/
|
||||||
|
caddy reload --config /Users/accusys/momentry/etc/Caddyfile
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Caddy 恢復完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_sftpgo() {
|
||||||
|
local timestamp=$1
|
||||||
|
log "恢復 SftpGo..."
|
||||||
|
|
||||||
|
# 停止 SFTPGo
|
||||||
|
pkill -f sftpgo || true
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# 恢復配置
|
||||||
|
local cfg_backup=$(ls "$BACKUP_ROOT/sftpgo"/sftpgo_cfg_${timestamp}.tar.gz 2>/dev/null | head -1)
|
||||||
|
if [ -n "$cfg_backup" ]; then
|
||||||
|
rm -rf /Users/accusys/momentry/etc/sftpgo
|
||||||
|
tar -xzf "$cfg_backup" -C /Users/accusys/momentry/etc/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 恢復 PostgreSQL 數據庫
|
||||||
|
local db_backup=$(ls "$BACKUP_ROOT/sftpgo"/sftpgo_db_${timestamp}.sql.gz 2>/dev/null | head -1)
|
||||||
|
if [ -n "$db_backup" ]; then
|
||||||
|
# 確保數據庫存在
|
||||||
|
PGPASSWORD="$PG_PASSWORD" psql -U "$PG_USER" -h localhost -d postgres -c "DROP DATABASE IF EXISTS sftpgo;" 2>/dev/null
|
||||||
|
PGPASSWORD="$PG_PASSWORD" psql -U "$PG_USER" -h localhost -d postgres -c "CREATE DATABASE sftpgo OWNER $SFTPGO_USER;" 2>/dev/null
|
||||||
|
gunzip -c "$db_backup" | PGPASSWORD="$SFTPGO_PASSWORD" psql -U "$SFTPGO_USER" -h localhost -d sftpgo 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 重啟 SFTPGo
|
||||||
|
cd /Users/accusys/momentry/var/sftpgo
|
||||||
|
/opt/homebrew/opt/sftpgo/bin/sftpgo serve --config-file /Users/accusys/momentry/etc/sftpgo/sftpgo.json &
|
||||||
|
|
||||||
|
log_success "SftpGo 恢復完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_mongodb() {
|
||||||
|
local timestamp=$1
|
||||||
|
log "恢復 MongoDB..."
|
||||||
|
|
||||||
|
# 解壓縮到臨時目錄
|
||||||
|
local MONGO_RESTORE_DIR="/tmp/mongodb_restore_${timestamp}"
|
||||||
|
mkdir -p "$MONGO_RESTORE_DIR"
|
||||||
|
|
||||||
|
local data_backup=$(ls "$BACKUP_ROOT/mongodb"/mongodb_data_${timestamp}.tar.gz 2>/dev/null | head -1)
|
||||||
|
if [ -n "$data_backup" ]; then
|
||||||
|
tar -xzf "$data_backup" -C "$MONGO_RESTORE_DIR/"
|
||||||
|
|
||||||
|
# 使用 mongorestore 恢復
|
||||||
|
if [ -n "$MONGODB_PASSWORD" ]; then
|
||||||
|
mongorestore --uri="mongodb://localhost:27017" \
|
||||||
|
--username="$MONGODB_USER" \
|
||||||
|
--password="$MONGODB_PASSWORD" \
|
||||||
|
--authenticationDatabase=admin \
|
||||||
|
--drop \
|
||||||
|
--dir="$MONGO_RESTORE_DIR" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
mongorestore --uri="mongodb://localhost:27017" \
|
||||||
|
--drop \
|
||||||
|
--dir="$MONGO_RESTORE_DIR" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$MONGO_RESTORE_DIR"
|
||||||
|
else
|
||||||
|
log_warn "MongoDB: 未找到備份文件"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "MongoDB 恢復完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_php() {
|
||||||
|
local timestamp=$1
|
||||||
|
log "恢復 PHP..."
|
||||||
|
|
||||||
|
local cfg_backup=$(ls "$BACKUP_ROOT/php"/php_cfg_${timestamp}.tar.gz 2>/dev/null | head -1)
|
||||||
|
if [ -n "$cfg_backup" ]; then
|
||||||
|
rm -rf /Users/accusys/momentry/etc/php/8.5
|
||||||
|
tar -xzf "$cfg_backup" -C /Users/accusys/momentry/etc/php/
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "PHP 恢復完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_momentry_output() {
|
||||||
|
local timestamp=$1
|
||||||
|
log "恢復 Momentry Output..."
|
||||||
|
|
||||||
|
# v2: Output 目錄可能有多個版本,嘗試 v2 版本再回退到舊版本
|
||||||
|
local output_backup=""
|
||||||
|
|
||||||
|
# 嘗試 v2 版本
|
||||||
|
output_backup=$(ls "$BACKUP_ROOT/momentry"/momentry_output_v2_${timestamp}.tar.gz 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
# 如果沒有 v2 版本,嘗試舊格式
|
||||||
|
if [ -z "$output_backup" ]; then
|
||||||
|
output_backup=$(ls "$BACKUP_ROOT/momentry"/momentry_output_${timestamp}.tar.gz 2>/dev/null | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$output_backup" ]; then
|
||||||
|
rm -rf /Users/accusys/momentry/output
|
||||||
|
mkdir -p /Users/accusys/momentry
|
||||||
|
tar -xzf "$output_backup" -C /Users/accusys/momentry/
|
||||||
|
log "Momentry Output: 恢復 $(basename $output_backup)"
|
||||||
|
else
|
||||||
|
log_warn "Momentry Output: 未找到備份檔案"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Momentry Output 恢復完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
#===============================================================================
|
||||||
|
# 主程序
|
||||||
|
#===============================================================================
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local command=${1:-all}
|
||||||
|
local service=${2:-}
|
||||||
|
local type=${3:-}
|
||||||
|
|
||||||
|
# 確保日誌目錄存在
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "=========================================="
|
||||||
|
log "Momentry 備份系統"
|
||||||
|
log "時間戳: $TIMESTAMP"
|
||||||
|
log "=========================================="
|
||||||
|
|
||||||
|
case $command in
|
||||||
|
restore | rollback)
|
||||||
|
if [ -z "$service" ]; then
|
||||||
|
log_error "請指定恢復時間戳 (YYYYMMDD_HHMMSS 或 v2_YYYYMMDD_HHMMSS)"
|
||||||
|
echo "示例: $0 restore v2_20260325_030000"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "開始恢復到斷點: $service"
|
||||||
|
|
||||||
|
for svc in "${SERVICES[@]}"; do
|
||||||
|
case $svc in
|
||||||
|
postgresql) restore_postgresql "$service" ;;
|
||||||
|
redis) restore_redis "$service" ;;
|
||||||
|
mariadb) restore_mariadb "$service" ;;
|
||||||
|
n8n) restore_n8n "$service" ;;
|
||||||
|
qdrant) restore_qdrant "$service" ;;
|
||||||
|
gitea) restore_gitea "$service" ;;
|
||||||
|
ollama) restore_ollama "$service" ;;
|
||||||
|
caddy) restore_caddy "$service" ;;
|
||||||
|
sftpgo) restore_sftpgo "$service" ;;
|
||||||
|
mongodb) restore_mongodb "$service" ;;
|
||||||
|
php) restore_php "$service" ;;
|
||||||
|
momentry_output) restore_momentry_output "$service" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
log "=========================================="
|
||||||
|
log_success "恢復完成!"
|
||||||
|
log "=========================================="
|
||||||
|
;;
|
||||||
|
|
||||||
|
list)
|
||||||
|
log "可用時間點:"
|
||||||
|
for dir in "$BACKUP_ROOT"/*/; do
|
||||||
|
local svc=$(basename "$dir")
|
||||||
|
echo " $svc:"
|
||||||
|
ls -1 "$dir"*.tar.gz "$dir"*.sql.gz "$dir"*.rdb 2>/dev/null |
|
||||||
|
sed 's/.*\([0-9]\{8\}\_[0-9]\{6\}\).*/\1/' | sort -u | sed 's/^/ /'
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
|
||||||
|
status)
|
||||||
|
log "備份狀態:"
|
||||||
|
echo ""
|
||||||
|
for svc in "${SERVICES[@]}"; do
|
||||||
|
local date_part="${TIMESTAMP#*_}" # Remove v2_ prefix
|
||||||
|
date_part="${date_part:0:8}" # Extract YYYYMMDD
|
||||||
|
local latest=$(find "$BACKUP_ROOT/$svc" \( -name "*_${date_part}_*" -o -name "*_v2_${date_part}_*" \) -type f 2>/dev/null | head -1)
|
||||||
|
if [ -n "$latest" ]; then
|
||||||
|
local size=$(du -h "$latest" | cut -f1)
|
||||||
|
echo -e " $svc: ${GREEN}✓${NC} $size"
|
||||||
|
else
|
||||||
|
echo -e " $svc: ${RED}✗${NC}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
|
||||||
|
all)
|
||||||
|
# 備份所有服務
|
||||||
|
for svc in "${SERVICES[@]}"; do
|
||||||
|
case $svc in
|
||||||
|
postgresql) backup_postgresql "$type" ;;
|
||||||
|
redis) backup_redis "$type" ;;
|
||||||
|
mariadb) backup_mariadb "$type" ;;
|
||||||
|
wordpress) backup_wordpress_files ;;
|
||||||
|
n8n) backup_n8n "$type" ;;
|
||||||
|
qdrant) backup_qdrant "$type" ;;
|
||||||
|
gitea) backup_gitea "$type" ;;
|
||||||
|
ollama) backup_ollama "$type" ;;
|
||||||
|
caddy) backup_caddy "$type" ;;
|
||||||
|
sftpgo) backup_sftpgo "$type" ;;
|
||||||
|
mongodb) backup_mongodb "$type" ;;
|
||||||
|
php) backup_php "$type" ;;
|
||||||
|
momentry_output) backup_momentry_output "$type" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
log "=========================================="
|
||||||
|
log_success "所有備份完成! 時間戳: $TIMESTAMP"
|
||||||
|
log "=========================================="
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
# 備份特定服務
|
||||||
|
if [ -n "$service" ]; then
|
||||||
|
case $service in
|
||||||
|
postgresql) backup_postgresql "$type" ;;
|
||||||
|
redis) backup_redis "$type" ;;
|
||||||
|
mariadb) backup_mariadb "$type" ;;
|
||||||
|
wordpress) backup_wordpress_files ;;
|
||||||
|
n8n) backup_n8n "$type" ;;
|
||||||
|
qdrant) backup_qdrant "$type" ;;
|
||||||
|
gitea) backup_gitea "$type" ;;
|
||||||
|
ollama) backup_ollama "$type" ;;
|
||||||
|
caddy) backup_caddy "$type" ;;
|
||||||
|
sftpgo) backup_sftpgo "$type" ;;
|
||||||
|
mongodb) backup_mongodb "$type" ;;
|
||||||
|
php) backup_php "$type" ;;
|
||||||
|
momentry_output) backup_momentry_output "$type" ;;
|
||||||
|
*)
|
||||||
|
log_error "未知服務: $service"
|
||||||
|
echo "可用服務: ${SERVICES[*]}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
log_error "請指定命令或服務"
|
||||||
|
echo "用法: $0 [命令] [服務] [類型]"
|
||||||
|
echo ""
|
||||||
|
echo "命令:"
|
||||||
|
echo " all - 備份所有服務 (默認)"
|
||||||
|
echo " <service> - 備份特定服務"
|
||||||
|
echo " restore - 恢復到指定斷點"
|
||||||
|
echo " list - 列出可用時間點"
|
||||||
|
echo " status - 顯示備份狀態"
|
||||||
|
echo ""
|
||||||
|
echo "服務: ${SERVICES[*]}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Regular → Executable
+3
-3
@@ -82,9 +82,9 @@ def generate_caption_with_llava(
|
|||||||
"""Generate caption using LLaVA model"""
|
"""Generate caption using LLaVA model"""
|
||||||
try:
|
try:
|
||||||
# Try to use transformers with LLaVA
|
# Try to use transformers with LLaVA
|
||||||
from transformers import AutoProcessor, AutoModelForVision2Seq
|
from transformers import AutoProcessor, AutoModelForVision2Seq # noqa: F401
|
||||||
import torch
|
import torch # noqa: F401
|
||||||
from PIL import Image
|
from PIL import Image # noqa: F401
|
||||||
|
|
||||||
# Note: This requires llava-hf/llava-1.5-7b-hf or similar
|
# Note: This requires llava-hf/llava-1.5-7b-hf or similar
|
||||||
# For now, return a placeholder
|
# For now, return a placeholder
|
||||||
|
|||||||
@@ -8,14 +8,24 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import signal
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
from redis_publisher import RedisPublisher
|
from redis_publisher import RedisPublisher
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
print(f"OCR: Received signal {signum}, exiting...")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def process_ocr(video_path: str, output_path: str, uuid: str = ""):
|
def process_ocr(video_path: str, output_path: str, uuid: str = ""):
|
||||||
"""Process video for OCR using EasyOCR"""
|
"""Process video for OCR using EasyOCR"""
|
||||||
|
|
||||||
|
# Set up signal handlers
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
publisher = RedisPublisher(uuid) if uuid else None
|
publisher = RedisPublisher(uuid) if uuid else None
|
||||||
if publisher:
|
if publisher:
|
||||||
publisher.info("ocr", "OCR_START")
|
publisher.info("ocr", "OCR_START")
|
||||||
|
|||||||
@@ -8,14 +8,24 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import signal
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
from redis_publisher import RedisPublisher
|
from redis_publisher import RedisPublisher
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
print(f"POSE: Received signal {signum}, exiting...")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def process_pose(video_path: str, output_path: str, uuid: str = ""):
|
def process_pose(video_path: str, output_path: str, uuid: str = ""):
|
||||||
"""Process video for pose estimation using YOLOv8 Pose"""
|
"""Process video for pose estimation using YOLOv8 Pose"""
|
||||||
|
|
||||||
|
# Set up signal handlers
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
publisher = RedisPublisher(uuid) if uuid else None
|
publisher = RedisPublisher(uuid) if uuid else None
|
||||||
if publisher:
|
if publisher:
|
||||||
publisher.info("pose", "POSE_START")
|
publisher.info("pose", "POSE_START")
|
||||||
|
|||||||
Regular → Executable
+1
-1
@@ -63,7 +63,7 @@ def generate_parent_child_chunks(
|
|||||||
asr_segments = asr_data.get("segments", [])
|
asr_segments = asr_data.get("segments", [])
|
||||||
cut_scenes = cut_data.get("scenes", [])
|
cut_scenes = cut_data.get("scenes", [])
|
||||||
yolo_frames = yolo_data.get("frames", [])
|
yolo_frames = yolo_data.get("frames", [])
|
||||||
ocr_frames = ocr_data.get("frames", [])
|
_ocr_frames = ocr_data.get("frames", [])
|
||||||
|
|
||||||
# Create child chunks from ASR segments
|
# Create child chunks from ASR segments
|
||||||
asr_child_ids = []
|
asr_child_ids = []
|
||||||
|
|||||||
+147
-36
@@ -148,6 +148,30 @@ struct SearchRequest {
|
|||||||
uuid: Option<String>,
|
uuid: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct CacheToggleRequest {
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct CacheToggleResponse {
|
||||||
|
success: bool,
|
||||||
|
cache_enabled: bool,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct UnregisterRequest {
|
||||||
|
uuid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct UnregisterResponse {
|
||||||
|
success: bool,
|
||||||
|
uuid: String,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct SearchResult {
|
struct SearchResult {
|
||||||
uuid: String,
|
uuid: String,
|
||||||
@@ -175,7 +199,7 @@ struct N8nSearchHit {
|
|||||||
text: String,
|
text: String,
|
||||||
score: f32,
|
score: f32,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
media_url: Option<String>,
|
file_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -213,6 +237,26 @@ struct HybridSearchResponse {
|
|||||||
query: String,
|
query: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extract_text_from_content(content: &serde_json::Value) -> String {
|
||||||
|
content
|
||||||
|
.get("data")
|
||||||
|
.and_then(|data| data.get("text"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.or_else(|| content.get("text").and_then(|v| v.as_str()))
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_title_from_content(content: &serde_json::Value) -> String {
|
||||||
|
content
|
||||||
|
.get("data")
|
||||||
|
.and_then(|data| data.get("title"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.or_else(|| content.get("title").and_then(|v| v.as_str()))
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct LookupQuery {
|
struct LookupQuery {
|
||||||
path: Option<String>,
|
path: Option<String>,
|
||||||
@@ -513,10 +557,22 @@ async fn register(
|
|||||||
let mut width = 0u32;
|
let mut width = 0u32;
|
||||||
let mut height = 0u32;
|
let mut height = 0u32;
|
||||||
|
|
||||||
|
let mut fps = 0.0;
|
||||||
for stream in &probe_result.streams {
|
for stream in &probe_result.streams {
|
||||||
if stream.codec_type.as_deref() == Some("video") {
|
if stream.codec_type.as_deref() == Some("video") {
|
||||||
width = stream.width.unwrap_or(0);
|
width = stream.width.unwrap_or(0);
|
||||||
height = stream.height.unwrap_or(0);
|
height = stream.height.unwrap_or(0);
|
||||||
|
|
||||||
|
// Parse FPS from r_frame_rate (e.g., "60000/1001")
|
||||||
|
if let Some(frame_rate_str) = &stream.r_frame_rate {
|
||||||
|
if let Some((num_str, den_str)) = frame_rate_str.split_once('/') {
|
||||||
|
if let (Ok(num), Ok(den)) = (num_str.parse::<f64>(), den_str.parse::<f64>()) {
|
||||||
|
if den != 0.0 {
|
||||||
|
fps = num / den;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,7 +598,7 @@ async fn register(
|
|||||||
duration,
|
duration,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fps: 0.0,
|
fps,
|
||||||
probe_json: Some(json_str),
|
probe_json: Some(json_str),
|
||||||
storage: Default::default(),
|
storage: Default::default(),
|
||||||
status: VideoStatus::Pending,
|
status: VideoStatus::Pending,
|
||||||
@@ -575,6 +631,17 @@ async fn register(
|
|||||||
StatusCode::INTERNAL_SERVER_ERROR
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
db.update_monitor_job_video_id(job.id, video_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to update monitor job video_id for job {}: {}",
|
||||||
|
job.id,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
let _ = state.mongo_cache.invalidate_videos_list().await;
|
let _ = state.mongo_cache.invalidate_videos_list().await;
|
||||||
|
|
||||||
Ok(Json(RegisterResponse {
|
Ok(Json(RegisterResponse {
|
||||||
@@ -747,28 +814,27 @@ async fn search(
|
|||||||
.map_err(|e| anyhow::anyhow!("PG init failed: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("PG init failed: {}", e))?;
|
||||||
|
|
||||||
let search_results = if let Some(ref uuid) = req.uuid {
|
let search_results = if let Some(ref uuid) = req.uuid {
|
||||||
let query_f64: Vec<f64> = query_vector.iter().map(|&x| x as f64).collect();
|
qdrant.search_in_uuid(&query_vector, uuid, limit).await?
|
||||||
qdrant.search_in_uuid(&query_f64, uuid, limit).await?
|
|
||||||
} else {
|
} else {
|
||||||
qdrant.search(&query_vector, limit).await?
|
qdrant.search(&query_vector, limit).await?
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
for r in search_results {
|
for r in search_results {
|
||||||
if let Some(chunk) = pg.get_chunk_by_chunk_id(&r.chunk_id).await.ok().flatten() {
|
if let Some(chunk) = pg
|
||||||
let text = chunk
|
.get_chunk_by_chunk_id_and_uuid(&r.chunk_id, &r.uuid)
|
||||||
.content
|
.await
|
||||||
.get("text")
|
.ok()
|
||||||
.and_then(|v| v.as_str())
|
.flatten()
|
||||||
.unwrap_or("")
|
{
|
||||||
.to_string();
|
let text = extract_text_from_content(&chunk.content);
|
||||||
|
|
||||||
results.push(SearchResult {
|
results.push(SearchResult {
|
||||||
uuid: chunk.uuid.clone(),
|
uuid: chunk.uuid.clone(),
|
||||||
chunk_id: chunk.chunk_id.clone(),
|
chunk_id: chunk.chunk_id.clone(),
|
||||||
chunk_type: chunk.chunk_type.as_str().to_string(),
|
chunk_type: chunk.chunk_type.as_str().to_string(),
|
||||||
start_time: chunk.start_time,
|
start_time: chunk.start_time().seconds(),
|
||||||
end_time: chunk.end_time,
|
end_time: chunk.end_time().seconds(),
|
||||||
text,
|
text,
|
||||||
score: r.score,
|
score: r.score,
|
||||||
});
|
});
|
||||||
@@ -810,43 +876,36 @@ async fn n8n_search(
|
|||||||
.map_err(|e| anyhow::anyhow!("PG init failed: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("PG init failed: {}", e))?;
|
||||||
|
|
||||||
let search_results = if let Some(ref uuid) = req.uuid {
|
let search_results = if let Some(ref uuid) = req.uuid {
|
||||||
let query_f64: Vec<f64> = query_vector.iter().map(|&x| x as f64).collect();
|
qdrant.search_in_uuid(&query_vector, uuid, limit).await?
|
||||||
qdrant.search_in_uuid(&query_f64, uuid, limit).await?
|
|
||||||
} else {
|
} else {
|
||||||
qdrant.search(&query_vector, limit).await?
|
qdrant.search(&query_vector, limit).await?
|
||||||
};
|
};
|
||||||
|
|
||||||
let media_base = crate::core::config::MEDIA_BASE_URL.as_str();
|
|
||||||
let mut hits = Vec::new();
|
let mut hits = Vec::new();
|
||||||
|
|
||||||
for r in search_results {
|
for r in search_results {
|
||||||
if let Some(chunk) = pg.get_chunk_by_chunk_id(&r.chunk_id).await.ok().flatten() {
|
if let Some(chunk) = pg
|
||||||
let text = chunk
|
.get_chunk_by_chunk_id_and_uuid(&r.chunk_id, &r.uuid)
|
||||||
.content
|
.await
|
||||||
.get("text")
|
.ok()
|
||||||
.and_then(|v| v.as_str())
|
.flatten()
|
||||||
.unwrap_or("")
|
{
|
||||||
.to_string();
|
let text = extract_text_from_content(&chunk.content);
|
||||||
|
|
||||||
let title = chunk
|
let title = extract_title_from_content(&chunk.content);
|
||||||
.content
|
|
||||||
.get("title")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let media_url = if chunk.uuid.is_empty() {
|
let file_path = if chunk.uuid.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let video = pg.get_video_by_uuid(&chunk.uuid).await.ok().flatten();
|
let video = pg.get_video_by_uuid(&chunk.uuid).await.ok().flatten();
|
||||||
video.map(|v| format!("{}/{}", media_base, v.file_name))
|
video.map(|v| v.file_path)
|
||||||
};
|
};
|
||||||
|
|
||||||
hits.push(N8nSearchHit {
|
hits.push(N8nSearchHit {
|
||||||
id: chunk.chunk_id.clone(),
|
id: chunk.chunk_id.clone(),
|
||||||
vid: chunk.uuid.clone(),
|
vid: chunk.uuid.clone(),
|
||||||
start: chunk.start_time,
|
start: chunk.start_time().seconds(),
|
||||||
end: chunk.end_time,
|
end: chunk.end_time().seconds(),
|
||||||
title: if title.is_empty() {
|
title: if title.is_empty() {
|
||||||
format!("Chunk {}", chunk.chunk_id)
|
format!("Chunk {}", chunk.chunk_id)
|
||||||
} else {
|
} else {
|
||||||
@@ -854,7 +913,7 @@ async fn n8n_search(
|
|||||||
},
|
},
|
||||||
text,
|
text,
|
||||||
score: r.score,
|
score: r.score,
|
||||||
media_url,
|
file_path,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1300,10 +1359,60 @@ async fn get_job(
|
|||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn cache_toggle(
|
||||||
|
State(_state): State<AppState>,
|
||||||
|
Json(req): Json<CacheToggleRequest>,
|
||||||
|
) -> Result<Json<CacheToggleResponse>, StatusCode> {
|
||||||
|
tracing::info!("[cache_toggle] Setting cache enabled to: {}", req.enabled);
|
||||||
|
|
||||||
|
crate::core::config::set_cache_enabled(req.enabled);
|
||||||
|
|
||||||
|
let response = CacheToggleResponse {
|
||||||
|
success: true,
|
||||||
|
cache_enabled: req.enabled,
|
||||||
|
message: if req.enabled {
|
||||||
|
"Cache enabled".to_string()
|
||||||
|
} else {
|
||||||
|
"Cache disabled".to_string()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!("[cache_toggle] SUCCESS");
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unregister(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<UnregisterRequest>,
|
||||||
|
) -> Result<Json<UnregisterResponse>, StatusCode> {
|
||||||
|
tracing::info!("[unregister] Unregistering video: {}", req.uuid);
|
||||||
|
|
||||||
|
let db = &state.api_state.db;
|
||||||
|
|
||||||
|
match db.delete_video(&req.uuid).await {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::info!("[unregister] SUCCESS - deleted: {}", req.uuid);
|
||||||
|
Ok(Json(UnregisterResponse {
|
||||||
|
success: true,
|
||||||
|
uuid: req.uuid,
|
||||||
|
message: "Video unregistered successfully".to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("[unregister] ERROR - {}", e);
|
||||||
|
Ok(Json(UnregisterResponse {
|
||||||
|
success: false,
|
||||||
|
uuid: req.uuid,
|
||||||
|
message: format!("Failed to unregister: {}", e),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn start_server(host: &str, port: u16) -> anyhow::Result<()> {
|
pub async fn start_server(host: &str, port: u16) -> anyhow::Result<()> {
|
||||||
let _ = SERVER_START.set(Instant::now());
|
let _ = SERVER_START.set(Instant::now());
|
||||||
|
|
||||||
let embedder = std::sync::Arc::new(Embedder::new("nomic-embed-text:v1.5".to_string()));
|
let embedder = std::sync::Arc::new(Embedder::new("nomic-embed-text-v2-moe:latest".to_string()));
|
||||||
let mongo_cache = MongoCache::init().await?;
|
let mongo_cache = MongoCache::init().await?;
|
||||||
let redis_cache = RedisCache::new()?;
|
let redis_cache = RedisCache::new()?;
|
||||||
let db = PostgresDb::init().await?;
|
let db = PostgresDb::init().await?;
|
||||||
@@ -1311,7 +1420,7 @@ pub async fn start_server(host: &str, port: u16) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
embedder,
|
embedder,
|
||||||
embedder_model: "nomic-embed-text:v1.5".to_string(),
|
embedder_model: "nomic-embed-text-v2-moe:latest".to_string(),
|
||||||
mongo_cache,
|
mongo_cache,
|
||||||
redis_cache,
|
redis_cache,
|
||||||
api_state,
|
api_state,
|
||||||
@@ -1319,6 +1428,7 @@ pub async fn start_server(host: &str, port: u16) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let protected_routes = Router::new()
|
let protected_routes = Router::new()
|
||||||
.route("/api/v1/register", post(register))
|
.route("/api/v1/register", post(register))
|
||||||
|
.route("/api/v1/unregister", post(unregister))
|
||||||
.route("/api/v1/probe", post(probe))
|
.route("/api/v1/probe", post(probe))
|
||||||
.route("/api/v1/search", post(search))
|
.route("/api/v1/search", post(search))
|
||||||
.route("/api/v1/n8n/search", post(n8n_search))
|
.route("/api/v1/n8n/search", post(n8n_search))
|
||||||
@@ -1328,6 +1438,7 @@ pub async fn start_server(host: &str, port: u16) -> anyhow::Result<()> {
|
|||||||
.route("/api/v1/progress/:uuid", get(get_progress))
|
.route("/api/v1/progress/:uuid", get(get_progress))
|
||||||
.route("/api/v1/jobs", get(list_jobs))
|
.route("/api/v1/jobs", get(list_jobs))
|
||||||
.route("/api/v1/jobs/:uuid", get(get_job))
|
.route("/api/v1/jobs/:uuid", get(get_job))
|
||||||
|
.route("/api/v1/config/cache", post(cache_toggle))
|
||||||
.layer(axum::middleware::from_fn_with_state(
|
.layer(axum::middleware::from_fn_with_state(
|
||||||
state.api_state.clone(),
|
state.api_state.clone(),
|
||||||
api_key_validation,
|
api_key_validation,
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use momentry_core::core::config;
|
||||||
|
use momentry_core::core::db::PostgresDb;
|
||||||
|
use momentry_core::core::processor::asrx::AsrxResult;
|
||||||
|
use momentry_core::core::processor::face::FaceResult;
|
||||||
|
use momentry_core::core::processor::ocr::OcrResult;
|
||||||
|
use momentry_core::core::processor::pose::PoseResult;
|
||||||
|
use momentry_core::core::processor::yolo::{YoloPythonResult, YoloResult};
|
||||||
|
use momentry_core::worker::processor::ProcessorPool;
|
||||||
|
use serde_json;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
// Initialize tracing
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
// Database connection
|
||||||
|
let db_url = config::DATABASE_URL.clone();
|
||||||
|
let db = PostgresDb::new(&db_url).await?;
|
||||||
|
|
||||||
|
let uuid = "9760d0820f0cf9a7";
|
||||||
|
|
||||||
|
// Load OCR result
|
||||||
|
let ocr_json =
|
||||||
|
fs::read_to_string("/Users/accusys/momentry/output/job_2_ocr_1774475908877.json")?;
|
||||||
|
let ocr_result: OcrResult = serde_json::from_str(&ocr_json)?;
|
||||||
|
println!("Loaded OCR result with {} frames", ocr_result.frames.len());
|
||||||
|
|
||||||
|
// Load FACE result
|
||||||
|
let face_json =
|
||||||
|
fs::read_to_string("/Users/accusys/momentry/output/job_2_face_1774475908878.json")?;
|
||||||
|
let face_result: FaceResult = serde_json::from_str(&face_json)?;
|
||||||
|
println!(
|
||||||
|
"Loaded FACE result with {} frames",
|
||||||
|
face_result.frames.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load POSE result
|
||||||
|
let pose_json =
|
||||||
|
fs::read_to_string("/Users/accusys/momentry/output/job_2_pose_1774475908880.json")?;
|
||||||
|
let pose_result: PoseResult = serde_json::from_str(&pose_json)?;
|
||||||
|
println!(
|
||||||
|
"Loaded POSE result with {} frames",
|
||||||
|
pose_result.frames.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load ASRX result
|
||||||
|
let asrx_json =
|
||||||
|
fs::read_to_string("/Users/accusys/momentry/output/job_2_asrx_1774475908887.json")?;
|
||||||
|
let asrx_result: AsrxResult = serde_json::from_str(&asrx_json)?;
|
||||||
|
println!(
|
||||||
|
"Loaded ASRX result with {} segments",
|
||||||
|
asrx_result.segments.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load YOLO result
|
||||||
|
let yolo_json =
|
||||||
|
fs::read_to_string("/Users/accusys/momentry/output/job_2_yolo_1774475908875.json")?;
|
||||||
|
let python_result: YoloPythonResult = serde_json::from_str(&yolo_json)?;
|
||||||
|
let yolo_result = python_result.to_yolo_result();
|
||||||
|
println!(
|
||||||
|
"Loaded YOLO result with {} frames",
|
||||||
|
yolo_result.frames.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store chunks using ProcessorPool's static methods
|
||||||
|
println!("Storing OCR chunks...");
|
||||||
|
ProcessorPool::store_ocr_chunks(&db, uuid, &ocr_result).await?;
|
||||||
|
println!("Storing FACE chunks...");
|
||||||
|
ProcessorPool::store_face_chunks(&db, uuid, &face_result).await?;
|
||||||
|
println!("Storing POSE chunks...");
|
||||||
|
ProcessorPool::store_pose_chunks(&db, uuid, &pose_result).await?;
|
||||||
|
println!("Storing ASRX chunks...");
|
||||||
|
ProcessorPool::store_asrx_chunks(&db, uuid, &asrx_result).await?;
|
||||||
|
println!("Storing YOLO chunks...");
|
||||||
|
ProcessorPool::store_yolo_chunks(&db, uuid, &yolo_result).await?;
|
||||||
|
|
||||||
|
println!("All trace chunks stored successfully!");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ impl ChunkSplitter {
|
|||||||
|
|
||||||
while current_time < duration {
|
while current_time < duration {
|
||||||
let end_time = (current_time + self.time_based_duration).min(duration);
|
let end_time = (current_time + self.time_based_duration).min(duration);
|
||||||
chunks.push(Chunk::new(
|
chunks.push(Chunk::from_seconds(
|
||||||
0, // file_id
|
0, // file_id
|
||||||
uuid.to_string(),
|
uuid.to_string(),
|
||||||
index,
|
index,
|
||||||
@@ -45,7 +45,7 @@ impl ChunkSplitter {
|
|||||||
let mut chunks = Vec::new();
|
let mut chunks = Vec::new();
|
||||||
|
|
||||||
for (index, segment) in asr_segments.iter().enumerate() {
|
for (index, segment) in asr_segments.iter().enumerate() {
|
||||||
chunks.push(Chunk::new(
|
chunks.push(Chunk::from_seconds(
|
||||||
0, // file_id
|
0, // file_id
|
||||||
uuid.to_string(),
|
uuid.to_string(),
|
||||||
index as u32,
|
index as u32,
|
||||||
|
|||||||
+102
-8
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::core::time::FrameTime;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||||
@@ -46,10 +47,11 @@ pub struct Chunk {
|
|||||||
pub chunk_index: u32,
|
pub chunk_index: u32,
|
||||||
pub chunk_type: ChunkType,
|
pub chunk_type: ChunkType,
|
||||||
pub rule: ChunkRule,
|
pub rule: ChunkRule,
|
||||||
pub start_time: f64,
|
/// Frames per second (can be fractional, e.g., 29.97, 23.976)
|
||||||
pub end_time: f64,
|
|
||||||
pub fps: f64,
|
pub fps: f64,
|
||||||
|
/// Start frame (0-based)
|
||||||
pub start_frame: i64,
|
pub start_frame: i64,
|
||||||
|
/// End frame (exclusive)
|
||||||
pub end_frame: i64,
|
pub end_frame: i64,
|
||||||
pub text_content: Option<String>,
|
pub text_content: Option<String>,
|
||||||
pub content: serde_json::Value,
|
pub content: serde_json::Value,
|
||||||
@@ -62,6 +64,13 @@ pub struct Chunk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Chunk {
|
impl Chunk {
|
||||||
|
/// Creates a new chunk from frame counts.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `start_frame` - Start frame (0-based)
|
||||||
|
/// * `end_frame` - End frame (exclusive)
|
||||||
|
/// * `fps` - Frames per second (can be fractional)
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
file_id: i32,
|
file_id: i32,
|
||||||
@@ -69,13 +78,11 @@ impl Chunk {
|
|||||||
chunk_index: u32,
|
chunk_index: u32,
|
||||||
chunk_type: ChunkType,
|
chunk_type: ChunkType,
|
||||||
rule: ChunkRule,
|
rule: ChunkRule,
|
||||||
start_time: f64,
|
start_frame: i64,
|
||||||
end_time: f64,
|
end_frame: i64,
|
||||||
fps: f64,
|
fps: f64,
|
||||||
content: serde_json::Value,
|
content: serde_json::Value,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let start_frame = (start_time * fps) as i64;
|
|
||||||
let end_frame = (end_time * fps) as i64;
|
|
||||||
let chunk_id = format!("{}_{:04}", chunk_type.as_str(), chunk_index);
|
let chunk_id = format!("{}_{:04}", chunk_type.as_str(), chunk_index);
|
||||||
Self {
|
Self {
|
||||||
file_id,
|
file_id,
|
||||||
@@ -84,8 +91,6 @@ impl Chunk {
|
|||||||
chunk_index,
|
chunk_index,
|
||||||
chunk_type,
|
chunk_type,
|
||||||
rule,
|
rule,
|
||||||
start_time,
|
|
||||||
end_time,
|
|
||||||
fps,
|
fps,
|
||||||
start_frame,
|
start_frame,
|
||||||
end_frame,
|
end_frame,
|
||||||
@@ -100,6 +105,95 @@ impl Chunk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new chunk from seconds (legacy conversion).
|
||||||
|
///
|
||||||
|
/// This is useful for migrating from older systems that store time as seconds.
|
||||||
|
/// The frame counts are calculated by rounding `seconds * fps`.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn from_seconds(
|
||||||
|
file_id: i32,
|
||||||
|
uuid: String,
|
||||||
|
chunk_index: u32,
|
||||||
|
chunk_type: ChunkType,
|
||||||
|
rule: ChunkRule,
|
||||||
|
start_time: f64,
|
||||||
|
end_time: f64,
|
||||||
|
fps: f64,
|
||||||
|
content: serde_json::Value,
|
||||||
|
) -> Self {
|
||||||
|
let start_frame = (start_time * fps).round() as i64;
|
||||||
|
let end_frame = (end_time * fps).round() as i64;
|
||||||
|
Self::new(
|
||||||
|
file_id,
|
||||||
|
uuid,
|
||||||
|
chunk_index,
|
||||||
|
chunk_type,
|
||||||
|
rule,
|
||||||
|
start_frame,
|
||||||
|
end_frame,
|
||||||
|
fps,
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the start time as a `FrameTime`.
|
||||||
|
pub fn start_time(&self) -> FrameTime {
|
||||||
|
FrameTime::from_frames(self.start_frame, self.fps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the end time as a `FrameTime`.
|
||||||
|
pub fn end_time(&self) -> FrameTime {
|
||||||
|
FrameTime::from_frames(self.end_frame, self.fps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the duration in frames.
|
||||||
|
pub fn duration_frames(&self) -> i64 {
|
||||||
|
self.end_frame - self.start_frame
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the duration in seconds.
|
||||||
|
pub fn duration_seconds(&self) -> f64 {
|
||||||
|
self.duration_frames() as f64 / self.fps
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the start time as "seconds.frame" (e.g., "123.04").
|
||||||
|
pub fn format_start_sec_frame(&self) -> String {
|
||||||
|
self.start_time().format_sec_frame()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the end time as "seconds.frame" (e.g., "456.15").
|
||||||
|
pub fn format_end_sec_frame(&self) -> String {
|
||||||
|
self.end_time().format_sec_frame()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the start time as "HH:MM:SS".
|
||||||
|
pub fn format_start_hms(&self) -> String {
|
||||||
|
self.start_time().format_hms()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the end time as "HH:MM:SS".
|
||||||
|
pub fn format_end_hms(&self) -> String {
|
||||||
|
self.end_time().format_hms()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the start time as "HH:MM:SS.FF".
|
||||||
|
pub fn format_start_hms_frame(&self) -> String {
|
||||||
|
self.start_time().format_hms_frame()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the end time as "HH:MM:SS.FF".
|
||||||
|
pub fn format_end_hms_frame(&self) -> String {
|
||||||
|
self.end_time().format_hms_frame()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a tuple of (start_seconds, end_seconds) for compatibility.
|
||||||
|
///
|
||||||
|
/// This is provided for backward compatibility during migration.
|
||||||
|
/// Prefer using `start_time()` and `end_time()` methods.
|
||||||
|
pub fn time_range_seconds(&self) -> (f64, f64) {
|
||||||
|
(self.start_time().seconds(), self.end_time().seconds())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
|
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
|
||||||
self.metadata = Some(metadata);
|
self.metadata = Some(metadata);
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
pub static RUNTIME_CACHE_ENABLED: Lazy<RwLock<bool>> = Lazy::new(|| {
|
||||||
|
let initial = env::var("MONGODB_CACHE_ENABLED")
|
||||||
|
.unwrap_or_else(|_| "true".to_string())
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(true);
|
||||||
|
RwLock::new(initial)
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn get_cache_enabled() -> bool {
|
||||||
|
*RUNTIME_CACHE_ENABLED.read().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_cache_enabled(enabled: bool) {
|
||||||
|
*RUNTIME_CACHE_ENABLED.write().unwrap() = enabled;
|
||||||
|
tracing::info!("Cache enabled set to: {}", enabled);
|
||||||
|
}
|
||||||
|
|
||||||
pub static DATABASE_URL: Lazy<String> = Lazy::new(|| {
|
pub static DATABASE_URL: Lazy<String> = Lazy::new(|| {
|
||||||
env::var("DATABASE_URL")
|
env::var("DATABASE_URL")
|
||||||
@@ -60,6 +78,15 @@ pub static SERVER_PORT: Lazy<u16> = Lazy::new(|| {
|
|||||||
pub static REDIS_KEY_PREFIX: Lazy<String> =
|
pub static REDIS_KEY_PREFIX: Lazy<String> =
|
||||||
Lazy::new(|| env::var("MOMENTRY_REDIS_PREFIX").unwrap_or_else(|_| "momentry:".to_string()));
|
Lazy::new(|| env::var("MOMENTRY_REDIS_PREFIX").unwrap_or_else(|_| "momentry:".to_string()));
|
||||||
|
|
||||||
|
pub static DATABASE_SCHEMA: Lazy<String> =
|
||||||
|
Lazy::new(|| env::var("DATABASE_SCHEMA").unwrap_or_else(|_| "public".to_string()));
|
||||||
|
|
||||||
|
pub static MONGODB_DATABASE: Lazy<String> =
|
||||||
|
Lazy::new(|| env::var("MONGODB_DATABASE").unwrap_or_else(|_| "momentry".to_string()));
|
||||||
|
|
||||||
|
pub static QDRANT_COLLECTION: Lazy<String> =
|
||||||
|
Lazy::new(|| env::var("QDRANT_COLLECTION").unwrap_or_else(|_| "momentry_rule1".to_string()));
|
||||||
|
|
||||||
pub mod processor {
|
pub mod processor {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
pub mod schema;
|
||||||
|
|
||||||
use crate::core::chunk::Chunk;
|
use crate::core::chunk::Chunk;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SearchResult {
|
pub struct SearchResult {
|
||||||
|
pub uuid: String,
|
||||||
pub chunk_id: String,
|
pub chunk_id: String,
|
||||||
pub score: f32,
|
pub score: f32,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,13 +28,15 @@ pub struct ChunkDocument {
|
|||||||
|
|
||||||
impl From<Chunk> for ChunkDocument {
|
impl From<Chunk> for ChunkDocument {
|
||||||
fn from(chunk: Chunk) -> Self {
|
fn from(chunk: Chunk) -> Self {
|
||||||
|
let start_time = chunk.start_time().seconds();
|
||||||
|
let end_time = chunk.end_time().seconds();
|
||||||
Self {
|
Self {
|
||||||
uuid: chunk.uuid,
|
uuid: chunk.uuid,
|
||||||
chunk_id: chunk.chunk_id,
|
chunk_id: chunk.chunk_id,
|
||||||
chunk_index: chunk.chunk_index,
|
chunk_index: chunk.chunk_index,
|
||||||
chunk_type: chunk.chunk_type.as_str().to_string(),
|
chunk_type: chunk.chunk_type.as_str().to_string(),
|
||||||
start_time: chunk.start_time,
|
start_time,
|
||||||
end_time: chunk.end_time,
|
end_time,
|
||||||
fps: chunk.fps,
|
fps: chunk.fps,
|
||||||
start_frame: chunk.start_frame,
|
start_frame: chunk.start_frame,
|
||||||
end_frame: chunk.end_frame,
|
end_frame: chunk.end_frame,
|
||||||
@@ -118,8 +120,6 @@ impl MongoDb {
|
|||||||
chunk_index: doc.chunk_index,
|
chunk_index: doc.chunk_index,
|
||||||
chunk_type,
|
chunk_type,
|
||||||
rule: ChunkRule::Rule1,
|
rule: ChunkRule::Rule1,
|
||||||
start_time: doc.start_time,
|
|
||||||
end_time: doc.end_time,
|
|
||||||
fps: doc.fps,
|
fps: doc.fps,
|
||||||
start_frame: doc.start_frame,
|
start_frame: doc.start_frame,
|
||||||
end_frame: doc.end_frame,
|
end_frame: doc.end_frame,
|
||||||
@@ -178,8 +178,6 @@ impl MongoDb {
|
|||||||
chunk_index: doc.chunk_index,
|
chunk_index: doc.chunk_index,
|
||||||
chunk_type,
|
chunk_type,
|
||||||
rule: ChunkRule::Rule1,
|
rule: ChunkRule::Rule1,
|
||||||
start_time: doc.start_time,
|
|
||||||
end_time: doc.end_time,
|
|
||||||
fps: doc.fps,
|
fps: doc.fps,
|
||||||
start_frame: doc.start_frame,
|
start_frame: doc.start_frame,
|
||||||
end_frame: doc.end_frame,
|
end_frame: doc.end_frame,
|
||||||
@@ -235,8 +233,6 @@ impl MongoDb {
|
|||||||
chunk_index: doc.chunk_index,
|
chunk_index: doc.chunk_index,
|
||||||
chunk_type,
|
chunk_type,
|
||||||
rule: ChunkRule::Rule1,
|
rule: ChunkRule::Rule1,
|
||||||
start_time: doc.start_time,
|
|
||||||
end_time: doc.end_time,
|
|
||||||
fps: doc.fps,
|
fps: doc.fps,
|
||||||
start_frame: doc.start_frame,
|
start_frame: doc.start_frame,
|
||||||
end_frame: doc.end_frame,
|
end_frame: doc.end_frame,
|
||||||
|
|||||||
+867
-314
File diff suppressed because it is too large
Load Diff
+106
-9
@@ -30,7 +30,13 @@ impl QdrantDb {
|
|||||||
let api_key = std::env::var("QDRANT_API_KEY")
|
let api_key = std::env::var("QDRANT_API_KEY")
|
||||||
.unwrap_or_else(|_| "Test3200Test3200Test3200".to_string());
|
.unwrap_or_else(|_| "Test3200Test3200Test3200".to_string());
|
||||||
let collection_name =
|
let collection_name =
|
||||||
std::env::var("QDRANT_COLLECTION").unwrap_or_else(|_| "chunks_v3".to_string());
|
std::env::var("QDRANT_COLLECTION").unwrap_or_else(|_| "momentry_rule1".to_string());
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"QdrantDb::new() - base_url: {}, collection_name: {}",
|
||||||
|
base_url,
|
||||||
|
collection_name
|
||||||
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
@@ -84,15 +90,21 @@ impl QdrantDb {
|
|||||||
|
|
||||||
pub async fn upsert_vector(
|
pub async fn upsert_vector(
|
||||||
&self,
|
&self,
|
||||||
_chunk_id: &str,
|
chunk_id: &str,
|
||||||
vector: &[f32],
|
vector: &[f32],
|
||||||
payload: VectorPayload,
|
payload: VectorPayload,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/collections/{}/points",
|
"{}/collections/{}/points?wait=true",
|
||||||
self.base_url, self.collection_name
|
self.base_url, self.collection_name
|
||||||
);
|
);
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"Qdrant upsert URL: {}, collection_name: {}",
|
||||||
|
url,
|
||||||
|
self.collection_name
|
||||||
|
);
|
||||||
|
|
||||||
let mut payload_map = HashMap::new();
|
let mut payload_map = HashMap::new();
|
||||||
payload_map.insert("uuid".to_string(), serde_json::json!(payload.uuid));
|
payload_map.insert("uuid".to_string(), serde_json::json!(payload.uuid));
|
||||||
payload_map.insert("chunk_id".to_string(), serde_json::json!(payload.chunk_id));
|
payload_map.insert("chunk_id".to_string(), serde_json::json!(payload.chunk_id));
|
||||||
@@ -109,7 +121,14 @@ impl QdrantDb {
|
|||||||
payload_map.insert("text".to_string(), serde_json::json!(text));
|
payload_map.insert("text".to_string(), serde_json::json!(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
let point_id = uuid::Uuid::new_v4().to_string();
|
// Generate consistent point ID from uuid and chunk_id
|
||||||
|
// Qdrant requires integer or UUID point IDs. We'll use a simple integer hash.
|
||||||
|
let point_id_str = format!("{}_{}", payload.uuid, chunk_id);
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
point_id_str.hash(&mut hasher);
|
||||||
|
let point_id = hasher.finish() as u64;
|
||||||
|
|
||||||
let body = serde_json::json!({
|
let body = serde_json::json!({
|
||||||
"points": [{
|
"points": [{
|
||||||
@@ -119,15 +138,41 @@ impl QdrantDb {
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
self.client
|
tracing::debug!(
|
||||||
|
"Upserting vector to Qdrant: chunk_id={}, uuid={}, vector_len={}",
|
||||||
|
chunk_id,
|
||||||
|
payload.uuid,
|
||||||
|
vector.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
.put(&url)
|
.put(&url)
|
||||||
.header("api-key", &self.api_key)
|
.header("api-key", &self.api_key)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.json(&body)
|
.json(&body)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.context("Failed to upsert vector in Qdrant")?;
|
.context("Failed to send upsert request to Qdrant")?;
|
||||||
|
|
||||||
|
// Check response status
|
||||||
|
let status = response.status();
|
||||||
|
let response_text = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| "Failed to read response".to_string());
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
tracing::error!("Qdrant upsert failed: {} - {}", status, response_text);
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Qdrant upsert failed with status {}: {}",
|
||||||
|
status,
|
||||||
|
response_text
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!("Qdrant response: {}", response_text);
|
||||||
|
tracing::info!("Successfully upserted vector for chunk: {}", chunk_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +198,22 @@ impl QdrantDb {
|
|||||||
.await
|
.await
|
||||||
.context("Failed to search Qdrant")?;
|
.context("Failed to search Qdrant")?;
|
||||||
|
|
||||||
|
// Check response status
|
||||||
|
let status = response.status();
|
||||||
|
let response_text = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| "Failed to read response".to_string());
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
tracing::error!("Qdrant search failed: {} - {}", status, response_text);
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Qdrant search failed with status {}: {}",
|
||||||
|
status,
|
||||||
|
response_text
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct QdrantSearchResult {
|
struct QdrantSearchResult {
|
||||||
result: Vec<QdrantPoint>,
|
result: Vec<QdrantPoint>,
|
||||||
@@ -166,12 +227,19 @@ impl QdrantDb {
|
|||||||
payload: HashMap<String, serde_json::Value>,
|
payload: HashMap<String, serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: QdrantSearchResult = response.json().await?;
|
let result: QdrantSearchResult = serde_json::from_str(&response_text)
|
||||||
|
.context("Failed to parse Qdrant search response")?;
|
||||||
|
|
||||||
let search_results: Vec<SearchResult> = result
|
let search_results: Vec<SearchResult> = result
|
||||||
.result
|
.result
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
|
let uuid = r
|
||||||
|
.payload
|
||||||
|
.get("uuid")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
.to_string();
|
||||||
let chunk_id = r
|
let chunk_id = r
|
||||||
.payload
|
.payload
|
||||||
.get("chunk_id")
|
.get("chunk_id")
|
||||||
@@ -179,6 +247,7 @@ impl QdrantDb {
|
|||||||
.unwrap_or("unknown")
|
.unwrap_or("unknown")
|
||||||
.to_string();
|
.to_string();
|
||||||
SearchResult {
|
SearchResult {
|
||||||
|
uuid,
|
||||||
chunk_id,
|
chunk_id,
|
||||||
score: r.score as f32,
|
score: r.score as f32,
|
||||||
}
|
}
|
||||||
@@ -190,7 +259,7 @@ impl QdrantDb {
|
|||||||
|
|
||||||
pub async fn search_in_uuid(
|
pub async fn search_in_uuid(
|
||||||
&self,
|
&self,
|
||||||
query_vector: &[f64],
|
query_vector: &[f32],
|
||||||
uuid: &str,
|
uuid: &str,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<SearchResult>> {
|
) -> Result<Vec<SearchResult>> {
|
||||||
@@ -225,6 +294,26 @@ impl QdrantDb {
|
|||||||
.await
|
.await
|
||||||
.context("Failed to search Qdrant")?;
|
.context("Failed to search Qdrant")?;
|
||||||
|
|
||||||
|
// Check response status
|
||||||
|
let status = response.status();
|
||||||
|
let response_text = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| "Failed to read response".to_string());
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
tracing::error!(
|
||||||
|
"Qdrant search_in_uuid failed: {} - {}",
|
||||||
|
status,
|
||||||
|
response_text
|
||||||
|
);
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Qdrant search_in_uuid failed with status {}: {}",
|
||||||
|
status,
|
||||||
|
response_text
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct QdrantSearchResult {
|
struct QdrantSearchResult {
|
||||||
result: Vec<QdrantPoint>,
|
result: Vec<QdrantPoint>,
|
||||||
@@ -238,12 +327,19 @@ impl QdrantDb {
|
|||||||
payload: HashMap<String, serde_json::Value>,
|
payload: HashMap<String, serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: QdrantSearchResult = response.json().await?;
|
let result: QdrantSearchResult = serde_json::from_str(&response_text)
|
||||||
|
.context("Failed to parse Qdrant search_in_uuid response")?;
|
||||||
|
|
||||||
let search_results: Vec<SearchResult> = result
|
let search_results: Vec<SearchResult> = result
|
||||||
.result
|
.result
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
|
let uuid = r
|
||||||
|
.payload
|
||||||
|
.get("uuid")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
.to_string();
|
||||||
let chunk_id = r
|
let chunk_id = r
|
||||||
.payload
|
.payload
|
||||||
.get("chunk_id")
|
.get("chunk_id")
|
||||||
@@ -251,6 +347,7 @@ impl QdrantDb {
|
|||||||
.unwrap_or("unknown")
|
.unwrap_or("unknown")
|
||||||
.to_string();
|
.to_string();
|
||||||
SearchResult {
|
SearchResult {
|
||||||
|
uuid,
|
||||||
chunk_id,
|
chunk_id,
|
||||||
score: r.score as f32,
|
score: r.score as f32,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
use crate::core::config::DATABASE_SCHEMA;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
pub static SCHEMA_PREFIX: Lazy<String> = Lazy::new(|| {
|
||||||
|
let schema = DATABASE_SCHEMA.as_str();
|
||||||
|
if schema == "public" {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("{}.", schema)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn table_name(table: &str) -> String {
|
||||||
|
let prefix = SCHEMA_PREFIX.as_str();
|
||||||
|
if prefix.is_empty() {
|
||||||
|
table.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}{}", prefix, table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_table_name_public() {
|
||||||
|
assert_eq!(table_name("videos"), "videos");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,8 +26,8 @@ impl SyncDb {
|
|||||||
let uuid = chunk.uuid.clone();
|
let uuid = chunk.uuid.clone();
|
||||||
let chunk_id = chunk.chunk_id.clone();
|
let chunk_id = chunk.chunk_id.clone();
|
||||||
let chunk_type = chunk.chunk_type.as_str().to_string();
|
let chunk_type = chunk.chunk_type.as_str().to_string();
|
||||||
let start_time = chunk.start_time;
|
let start_time = chunk.start_time().seconds();
|
||||||
let end_time = chunk.end_time;
|
let end_time = chunk.end_time().seconds();
|
||||||
|
|
||||||
let vector = self.embed_text(text).await?;
|
let vector = self.embed_text(text).await?;
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ impl SyncDb {
|
|||||||
let response = client
|
let response = client
|
||||||
.post("http://localhost:11434/api/embeddings")
|
.post("http://localhost:11434/api/embeddings")
|
||||||
.json(&json!({
|
.json(&json!({
|
||||||
"model": "nomic-embed-text",
|
"model": "nomic-embed-text-v2-moe:latest",
|
||||||
"prompt": text
|
"prompt": text
|
||||||
}))
|
}))
|
||||||
.send()
|
.send()
|
||||||
@@ -117,7 +117,7 @@ impl SyncDb {
|
|||||||
"language_probability": asr_result.language_probability,
|
"language_probability": asr_result.language_probability,
|
||||||
});
|
});
|
||||||
|
|
||||||
let chunk = Chunk::new(
|
let chunk = Chunk::from_seconds(
|
||||||
0, // file_id - will be set later
|
0, // file_id - will be set later
|
||||||
uuid.to_string(),
|
uuid.to_string(),
|
||||||
i as u32,
|
i as u32,
|
||||||
@@ -137,7 +137,8 @@ impl SyncDb {
|
|||||||
for chunk in chunks {
|
for chunk in chunks {
|
||||||
let text = chunk
|
let text = chunk
|
||||||
.content
|
.content
|
||||||
.get("text")
|
.get("data")
|
||||||
|
.and_then(|data| data.get("text"))
|
||||||
.and_then(|t| t.as_str())
|
.and_then(|t| t.as_str())
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ pub mod probe;
|
|||||||
pub mod processor;
|
pub mod processor;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
pub mod thumbnail;
|
pub mod thumbnail;
|
||||||
|
pub mod time;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use super::executor::PythonExecutor;
|
use super::executor::PythonExecutor;
|
||||||
|
|
||||||
const ASR_TIMEOUT: Duration = Duration::from_secs(3600);
|
const ASR_TIMEOUT: Duration = Duration::from_secs(1800); // 30 minutes
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct AsrResult {
|
pub struct AsrResult {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use libc;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -159,12 +160,16 @@ impl PythonExecutor {
|
|||||||
|
|
||||||
cmd.stdout(Stdio::piped());
|
cmd.stdout(Stdio::piped());
|
||||||
cmd.stderr(Stdio::piped());
|
cmd.stderr(Stdio::piped());
|
||||||
|
cmd.kill_on_drop(true);
|
||||||
|
// Create new process group for clean termination
|
||||||
|
cmd.process_group(0);
|
||||||
|
|
||||||
tracing::info!("[{}] Starting: {:?}", log_prefix, script_name);
|
tracing::info!("[{}] Starting: {:?}", log_prefix, script_name);
|
||||||
|
|
||||||
let mut child = cmd
|
let mut child = cmd
|
||||||
.spawn()
|
.spawn()
|
||||||
.with_context(|| format!("Failed to run {}", script_name))?;
|
.with_context(|| format!("Failed to run {}", script_name))?;
|
||||||
|
let child_pid = child.id();
|
||||||
|
|
||||||
let stdout = child.stdout.take().context("Failed to capture stdout")?;
|
let stdout = child.stdout.take().context("Failed to capture stdout")?;
|
||||||
let stderr = child.stderr.take().context("Failed to capture stderr")?;
|
let stderr = child.stderr.take().context("Failed to capture stderr")?;
|
||||||
@@ -220,6 +225,13 @@ impl PythonExecutor {
|
|||||||
Ok(Ok(())) => {}
|
Ok(Ok(())) => {}
|
||||||
Ok(Err(e)) => return Err(e),
|
Ok(Err(e)) => return Err(e),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
// Try to kill the entire process group
|
||||||
|
if let Some(pid) = child_pid {
|
||||||
|
let pgid = pid as i32;
|
||||||
|
unsafe {
|
||||||
|
libc::killpg(pgid, libc::SIGKILL);
|
||||||
|
}
|
||||||
|
}
|
||||||
child.kill().await.context("Failed to kill process")?;
|
child.kill().await.context("Failed to kill process")?;
|
||||||
anyhow::bail!("{} timed out after {:?}", script_name, duration);
|
anyhow::bail!("{} timed out after {:?}", script_name, duration);
|
||||||
}
|
}
|
||||||
|
|||||||
+159
-1
@@ -1,5 +1,6 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use super::executor::PythonExecutor;
|
use super::executor::PythonExecutor;
|
||||||
@@ -31,6 +32,90 @@ pub struct YoloObject {
|
|||||||
pub confidence: f32,
|
pub confidence: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New structs for parsing Python YOLO output
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct YoloPythonMetadata {
|
||||||
|
video_path: String,
|
||||||
|
fps: f64,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
total_frames: i64,
|
||||||
|
total_duration: f64,
|
||||||
|
processed_at: String,
|
||||||
|
auto_save_interval: i32,
|
||||||
|
auto_save_frames: i32,
|
||||||
|
status: String,
|
||||||
|
last_saved_at: String,
|
||||||
|
last_saved_frame: i64,
|
||||||
|
completed_at: Option<String>,
|
||||||
|
processing_time: Option<f64>,
|
||||||
|
total_detections: Option<i64>,
|
||||||
|
auto_save_count: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct YoloPythonDetection {
|
||||||
|
class_name: String,
|
||||||
|
confidence: f32,
|
||||||
|
x1: f32,
|
||||||
|
y1: f32,
|
||||||
|
x2: f32,
|
||||||
|
y2: f32,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
class_id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct YoloPythonFrame {
|
||||||
|
frame_number: u64,
|
||||||
|
time_seconds: f64,
|
||||||
|
time_formatted: String,
|
||||||
|
detections: Vec<YoloPythonDetection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct YoloPythonResult {
|
||||||
|
metadata: YoloPythonMetadata,
|
||||||
|
frames: HashMap<String, YoloPythonFrame>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YoloPythonResult {
|
||||||
|
pub fn to_yolo_result(&self) -> YoloResult {
|
||||||
|
let mut frames = Vec::new();
|
||||||
|
|
||||||
|
// Sort frames by frame number (key is string, but we parse as u64)
|
||||||
|
let mut frame_entries: Vec<_> = self.frames.iter().collect();
|
||||||
|
frame_entries.sort_by_key(|(key, _)| key.parse::<u64>().unwrap_or(0));
|
||||||
|
|
||||||
|
for (_, frame) in frame_entries {
|
||||||
|
let mut objects = Vec::new();
|
||||||
|
for detection in &frame.detections {
|
||||||
|
objects.push(YoloObject {
|
||||||
|
class_name: detection.class_name.clone(),
|
||||||
|
class_id: detection.class_id.unwrap_or(0),
|
||||||
|
x: detection.x1 as i32,
|
||||||
|
y: detection.y1 as i32,
|
||||||
|
width: detection.width,
|
||||||
|
height: detection.height,
|
||||||
|
confidence: detection.confidence,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
frames.push(YoloFrame {
|
||||||
|
frame: frame.frame_number,
|
||||||
|
timestamp: frame.time_seconds,
|
||||||
|
objects,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
YoloResult {
|
||||||
|
frame_count: frames.len() as u64,
|
||||||
|
fps: self.metadata.fps,
|
||||||
|
frames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn process_yolo(
|
pub async fn process_yolo(
|
||||||
video_path: &str,
|
video_path: &str,
|
||||||
output_path: &str,
|
output_path: &str,
|
||||||
@@ -63,9 +148,11 @@ pub async fn process_yolo(
|
|||||||
|
|
||||||
let json_str = std::fs::read_to_string(output_path).context("Failed to read YOLO output")?;
|
let json_str = std::fs::read_to_string(output_path).context("Failed to read YOLO output")?;
|
||||||
|
|
||||||
let result: YoloResult =
|
let python_result: YoloPythonResult =
|
||||||
serde_json::from_str(&json_str).context("Failed to parse YOLO output")?;
|
serde_json::from_str(&json_str).context("Failed to parse YOLO output")?;
|
||||||
|
|
||||||
|
let result = python_result.to_yolo_result();
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"[YOLO] Result: {} frames, {:.2} fps",
|
"[YOLO] Result: {} frames, {:.2} fps",
|
||||||
result.frame_count,
|
result.frame_count,
|
||||||
@@ -150,4 +237,75 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert!(result.frames.is_empty());
|
assert!(result.frames.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_yolo_python_result_parsing() {
|
||||||
|
// Sample JSON matching Python script output
|
||||||
|
let json = r#"{
|
||||||
|
"metadata": {
|
||||||
|
"video_path": "/test/video.mp4",
|
||||||
|
"fps": 22.0,
|
||||||
|
"width": 640,
|
||||||
|
"height": 360,
|
||||||
|
"total_frames": 3512,
|
||||||
|
"total_duration": 159.63636363636363,
|
||||||
|
"processed_at": "2026-03-26T05:20:48.230143",
|
||||||
|
"auto_save_interval": 30,
|
||||||
|
"auto_save_frames": 300,
|
||||||
|
"status": "completed",
|
||||||
|
"last_saved_at": "2026-03-26T05:23:22.791673",
|
||||||
|
"last_saved_frame": 0,
|
||||||
|
"completed_at": "2026-03-26T05:23:22.791666",
|
||||||
|
"processing_time": 154.5577518939972,
|
||||||
|
"total_detections": 12786,
|
||||||
|
"auto_save_count": 11
|
||||||
|
},
|
||||||
|
"frames": {
|
||||||
|
"13": {
|
||||||
|
"frame_number": 13,
|
||||||
|
"time_seconds": 0.545,
|
||||||
|
"time_formatted": "00:00:00",
|
||||||
|
"detections": [
|
||||||
|
{
|
||||||
|
"class_id": 0,
|
||||||
|
"class_name": "person",
|
||||||
|
"confidence": 0.8424218893051147,
|
||||||
|
"x1": 473.4156494140625,
|
||||||
|
"y1": 79.5609359741211,
|
||||||
|
"x2": 639.77783203125,
|
||||||
|
"y2": 303.8714294433594,
|
||||||
|
"width": 166,
|
||||||
|
"height": 224
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let python_result: YoloPythonResult = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(python_result.metadata.fps, 22.0);
|
||||||
|
assert_eq!(python_result.frames.len(), 1);
|
||||||
|
let frame = python_result.frames.get("13").unwrap();
|
||||||
|
assert_eq!(frame.frame_number, 13);
|
||||||
|
assert_eq!(frame.detections.len(), 1);
|
||||||
|
let detection = &frame.detections[0];
|
||||||
|
assert_eq!(detection.class_id, Some(0));
|
||||||
|
assert_eq!(detection.class_name, "person");
|
||||||
|
assert!((detection.confidence - 0.8424218893051147).abs() < 0.0001);
|
||||||
|
assert!((detection.x1 - 473.4156494140625).abs() < 0.0001);
|
||||||
|
|
||||||
|
// Convert to YoloResult
|
||||||
|
let yolo_result = python_result.to_yolo_result();
|
||||||
|
assert_eq!(yolo_result.frames.len(), 1);
|
||||||
|
assert_eq!(yolo_result.frames[0].frame, 13);
|
||||||
|
assert_eq!(yolo_result.frames[0].objects.len(), 1);
|
||||||
|
let obj = &yolo_result.frames[0].objects[0];
|
||||||
|
assert_eq!(obj.class_name, "person");
|
||||||
|
assert_eq!(obj.class_id, 0);
|
||||||
|
assert_eq!(obj.x, 473);
|
||||||
|
assert_eq!(obj.y, 79);
|
||||||
|
assert_eq!(obj.width, 166);
|
||||||
|
assert_eq!(obj.height, 224);
|
||||||
|
assert!((obj.confidence - 0.842421889).abs() < 0.0001);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,383 @@
|
|||||||
|
//! Frame-based time representation for video processing.
|
||||||
|
//!
|
||||||
|
//! This module provides a `FrameTime` struct that stores time as frame count
|
||||||
|
//! with a given FPS (frames per second). This avoids floating-point precision
|
||||||
|
//! issues when converting between seconds and frames.
|
||||||
|
//!
|
||||||
|
//! # Examples
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use momentry_core::time::FrameTime;
|
||||||
|
//!
|
||||||
|
//! // Create a FrameTime from frames
|
||||||
|
//! let time = FrameTime::from_frames(1234, 30.0);
|
||||||
|
//! assert_eq!(time.seconds(), 41.13333333333333);
|
||||||
|
//! assert_eq!(time.format_sec_frame(), "41.04");
|
||||||
|
//!
|
||||||
|
//! // Create from seconds (useful for migration)
|
||||||
|
//! let time = FrameTime::from_seconds(41.133333, 30.0);
|
||||||
|
//! assert_eq!(time.frames(), 1234);
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// Frame-based time representation.
|
||||||
|
///
|
||||||
|
/// Stores time as an integer frame count with a floating-point FPS.
|
||||||
|
/// All calculations are performed using integer frame counts to avoid
|
||||||
|
/// floating-point precision issues.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct FrameTime {
|
||||||
|
/// Frame count (0-based)
|
||||||
|
frames: i64,
|
||||||
|
/// Frames per second (can be fractional, e.g., 29.97, 23.976)
|
||||||
|
fps: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameTime {
|
||||||
|
/// Creates a new `FrameTime` from frame count and FPS.
|
||||||
|
///
|
||||||
|
/// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS.
|
||||||
|
pub fn from_frames(frames: i64, fps: f64) -> Self {
|
||||||
|
let fps = if fps <= 0.0 || !fps.is_finite() {
|
||||||
|
30.0
|
||||||
|
} else {
|
||||||
|
fps
|
||||||
|
};
|
||||||
|
Self { frames, fps }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `FrameTime` from seconds and FPS.
|
||||||
|
///
|
||||||
|
/// This is useful for migrating from existing time representations.
|
||||||
|
/// The frame count is calculated as `(seconds * fps).round() as i64`
|
||||||
|
/// to minimize precision loss.
|
||||||
|
///
|
||||||
|
/// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS.
|
||||||
|
pub fn from_seconds(seconds: f64, fps: f64) -> Self {
|
||||||
|
let fps = if fps <= 0.0 || !fps.is_finite() {
|
||||||
|
30.0
|
||||||
|
} else {
|
||||||
|
fps
|
||||||
|
};
|
||||||
|
let frames = (seconds * fps).round() as i64;
|
||||||
|
Self { frames, fps }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the frame count.
|
||||||
|
pub fn frames(&self) -> i64 {
|
||||||
|
self.frames
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the FPS (frames per second).
|
||||||
|
pub fn fps(&self) -> f64 {
|
||||||
|
self.fps
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the time in seconds as a floating-point value.
|
||||||
|
///
|
||||||
|
/// Note: This may have precision limitations for fractional FPS values.
|
||||||
|
/// For display purposes, use `format_sec_frame()` or `format_hms()` instead.
|
||||||
|
pub fn seconds(&self) -> f64 {
|
||||||
|
self.frames as f64 / self.fps
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the time as "seconds.frame" with fixed two-digit frame number.
|
||||||
|
///
|
||||||
|
/// The frame number is displayed as a zero-padded two-digit number
|
||||||
|
/// representing the frame within the current second.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// - `123.04` = 123 seconds, frame 4 (at 30 FPS, frame 4 = 0.133 seconds)
|
||||||
|
/// - `5.29` = 5 seconds, frame 29 (at 30 FPS, last frame of that second)
|
||||||
|
pub fn format_sec_frame(&self) -> String {
|
||||||
|
let total_seconds = self.frames as f64 / self.fps;
|
||||||
|
let seconds = total_seconds.floor() as i64;
|
||||||
|
// For fractional FPS, use ceil of fps for modulo operation
|
||||||
|
let fps_ceil = self.fps.ceil() as i64;
|
||||||
|
// Ensure fps_ceil > 0
|
||||||
|
let frames_in_second = if fps_ceil == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
self.frames % fps_ceil
|
||||||
|
};
|
||||||
|
// Handle negative frames
|
||||||
|
let frames_in_second = if frames_in_second < 0 {
|
||||||
|
// This shouldn't happen in practice
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
frames_in_second
|
||||||
|
};
|
||||||
|
format!("{}.{:02}", seconds, frames_in_second)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the time as "HH:MM:SS" (hours, minutes, seconds).
|
||||||
|
///
|
||||||
|
/// This displays whole seconds only, without frame information.
|
||||||
|
/// Useful for human-readable time displays.
|
||||||
|
pub fn format_hms(&self) -> String {
|
||||||
|
let total_seconds = self.seconds();
|
||||||
|
let hours = (total_seconds / 3600.0) as i64;
|
||||||
|
let minutes = ((total_seconds % 3600.0) / 60.0) as i64;
|
||||||
|
let seconds = (total_seconds % 60.0) as i64;
|
||||||
|
|
||||||
|
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the time as "HH:MM:SS.FF" (hours, minutes, seconds, frames).
|
||||||
|
///
|
||||||
|
/// Displays full time with frame information. Frames are shown as
|
||||||
|
/// zero-padded two-digit numbers.
|
||||||
|
pub fn format_hms_frame(&self) -> String {
|
||||||
|
let total_seconds = self.seconds();
|
||||||
|
let hours = (total_seconds / 3600.0) as i64;
|
||||||
|
let minutes = ((total_seconds % 3600.0) / 60.0) as i64;
|
||||||
|
let seconds = (total_seconds % 60.0) as i64;
|
||||||
|
// For fractional FPS, use ceil of fps for modulo operation
|
||||||
|
let fps_ceil = self.fps.ceil() as i64;
|
||||||
|
let frames_in_second = if fps_ceil == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
self.frames % fps_ceil
|
||||||
|
};
|
||||||
|
let frames_in_second = if frames_in_second < 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
frames_in_second
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{:02}:{:02}:{:02}.{:02}",
|
||||||
|
hours, minutes, seconds, frames_in_second
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds frames to this time, returning a new `FrameTime`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the FPS doesn't match.
|
||||||
|
pub fn add_frames(&self, frames: i64) -> Self {
|
||||||
|
Self {
|
||||||
|
frames: self.frames + frames,
|
||||||
|
fps: self.fps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subtracts frames from this time, returning a new `FrameTime`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the FPS doesn't match or if the result would be negative.
|
||||||
|
pub fn sub_frames(&self, frames: i64) -> Self {
|
||||||
|
assert!(
|
||||||
|
self.frames >= frames,
|
||||||
|
"Cannot subtract more frames than available"
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
frames: self.frames - frames,
|
||||||
|
fps: self.fps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds seconds to this time, returning a new `FrameTime`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the FPS doesn't match.
|
||||||
|
pub fn add_seconds(&self, seconds: f64) -> Self {
|
||||||
|
let frames_to_add = (seconds * self.fps).round() as i64;
|
||||||
|
self.add_frames(frames_to_add)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subtracts seconds from this time, returning a new `FrameTime`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the FPS doesn't match or if the result would be negative.
|
||||||
|
pub fn sub_seconds(&self, seconds: f64) -> Self {
|
||||||
|
let frames_to_sub = (seconds * self.fps).round() as i64;
|
||||||
|
self.sub_frames(frames_to_sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the duration between two `FrameTime` instances.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the FPS values don't match.
|
||||||
|
pub fn duration(&self, other: &FrameTime) -> FrameDuration {
|
||||||
|
assert!(
|
||||||
|
(self.fps - other.fps).abs() < f64::EPSILON * 2.0,
|
||||||
|
"FPS mismatch: {} != {}",
|
||||||
|
self.fps,
|
||||||
|
other.fps
|
||||||
|
);
|
||||||
|
|
||||||
|
let frame_diff = (self.frames - other.frames).abs();
|
||||||
|
FrameDuration::from_frames(frame_diff, self.fps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FrameTime {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.format_sec_frame())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Duration between two frame times.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct FrameDuration {
|
||||||
|
frames: i64,
|
||||||
|
fps: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameDuration {
|
||||||
|
/// Creates a duration from frame count and FPS.
|
||||||
|
/// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS.
|
||||||
|
pub fn from_frames(frames: i64, fps: f64) -> Self {
|
||||||
|
let fps = if fps <= 0.0 || !fps.is_finite() {
|
||||||
|
30.0
|
||||||
|
} else {
|
||||||
|
fps
|
||||||
|
};
|
||||||
|
Self { frames, fps }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a duration from seconds and FPS.
|
||||||
|
/// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS.
|
||||||
|
pub fn from_seconds(seconds: f64, fps: f64) -> Self {
|
||||||
|
let fps = if fps <= 0.0 || !fps.is_finite() {
|
||||||
|
30.0
|
||||||
|
} else {
|
||||||
|
fps
|
||||||
|
};
|
||||||
|
let frames = (seconds * fps).round() as i64;
|
||||||
|
Self { frames, fps }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the duration in frames.
|
||||||
|
pub fn frames(&self) -> i64 {
|
||||||
|
self.frames
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the duration in seconds.
|
||||||
|
pub fn seconds(&self) -> f64 {
|
||||||
|
self.frames as f64 / self.fps
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the duration as "seconds.frame" (same as `FrameTime`).
|
||||||
|
pub fn format_sec_frame(&self) -> String {
|
||||||
|
let temp_time = FrameTime::from_frames(self.frames, self.fps);
|
||||||
|
temp_time.format_sec_frame()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the duration as "HH:MM:SS".
|
||||||
|
pub fn format_hms(&self) -> String {
|
||||||
|
let temp_time = FrameTime::from_frames(self.frames, self.fps);
|
||||||
|
temp_time.format_hms()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FrameDuration {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.format_sec_frame())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_frames() {
|
||||||
|
let time = FrameTime::from_frames(150, 30.0);
|
||||||
|
assert_eq!(time.frames(), 150);
|
||||||
|
assert_eq!(time.fps(), 30.0);
|
||||||
|
assert_eq!(time.seconds(), 5.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_seconds() {
|
||||||
|
let time = FrameTime::from_seconds(5.0, 30.0);
|
||||||
|
assert_eq!(time.frames(), 150);
|
||||||
|
assert_eq!(time.seconds(), 5.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_sec_frame() {
|
||||||
|
let time = FrameTime::from_frames(123, 30.0);
|
||||||
|
assert_eq!(time.format_sec_frame(), "4.03");
|
||||||
|
|
||||||
|
let time = FrameTime::from_frames(29, 30.0);
|
||||||
|
assert_eq!(time.format_sec_frame(), "0.29");
|
||||||
|
|
||||||
|
let time = FrameTime::from_frames(60, 30.0);
|
||||||
|
assert_eq!(time.format_sec_frame(), "2.00");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_sec_frame_fractional_fps() {
|
||||||
|
// 29.97 fps (NTSC)
|
||||||
|
let time = FrameTime::from_frames(30, 29.97);
|
||||||
|
// 30 frames at 29.97 fps = 1.001 seconds = 1 second, frame 0
|
||||||
|
assert_eq!(time.format_sec_frame(), "1.00");
|
||||||
|
|
||||||
|
let time = FrameTime::from_frames(60, 29.97);
|
||||||
|
// 60 frames at 29.97 fps = 2.002 seconds = 2 seconds, frame 0
|
||||||
|
assert_eq!(time.format_sec_frame(), "2.00");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_hms() {
|
||||||
|
let time = FrameTime::from_frames(3661, 30.0); // 122.033 seconds = 2 minutes 2 seconds
|
||||||
|
assert_eq!(time.format_hms(), "00:02:02");
|
||||||
|
|
||||||
|
let time = FrameTime::from_frames(4500, 30.0); // 150 seconds = 2 minutes 30 seconds
|
||||||
|
assert_eq!(time.format_hms(), "00:02:30");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_hms_frame() {
|
||||||
|
let time = FrameTime::from_frames(123, 30.0); // 4 seconds, 3 frames
|
||||||
|
assert_eq!(time.format_hms_frame(), "00:00:04.03");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_sub_frames() {
|
||||||
|
let time = FrameTime::from_frames(100, 30.0);
|
||||||
|
let new_time = time.add_frames(50);
|
||||||
|
assert_eq!(new_time.frames(), 150);
|
||||||
|
|
||||||
|
let new_time = time.sub_frames(30);
|
||||||
|
assert_eq!(new_time.frames(), 70);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_sub_seconds() {
|
||||||
|
let time = FrameTime::from_frames(100, 30.0);
|
||||||
|
let new_time = time.add_seconds(2.0);
|
||||||
|
assert_eq!(new_time.frames(), 160); // 100 + 60
|
||||||
|
|
||||||
|
let new_time = time.sub_seconds(1.0);
|
||||||
|
assert_eq!(new_time.frames(), 70); // 100 - 30
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duration() {
|
||||||
|
let time1 = FrameTime::from_frames(200, 30.0);
|
||||||
|
let time2 = FrameTime::from_frames(150, 30.0);
|
||||||
|
let duration = time1.duration(&time2);
|
||||||
|
assert_eq!(duration.frames(), 50);
|
||||||
|
assert_eq!(duration.seconds(), 50.0 / 30.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_frame_duration() {
|
||||||
|
let duration = FrameDuration::from_frames(90, 30.0);
|
||||||
|
assert_eq!(duration.seconds(), 3.0);
|
||||||
|
assert_eq!(duration.format_sec_frame(), "3.00");
|
||||||
|
assert_eq!(duration.format_hms(), "00:00:03");
|
||||||
|
}
|
||||||
|
}
|
||||||
+75
-49
@@ -1,6 +1,7 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str;
|
use std::str;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
@@ -8,6 +9,7 @@ use std::sync::{Arc, Mutex};
|
|||||||
use momentry_core::core::api_key::{ApiKeyService, ApiKeyType};
|
use momentry_core::core::api_key::{ApiKeyService, ApiKeyType};
|
||||||
use momentry_core::core::chunk::types::{Chunk, ChunkRule, ChunkType};
|
use momentry_core::core::chunk::types::{Chunk, ChunkRule, ChunkType};
|
||||||
use momentry_core::core::db::Database;
|
use momentry_core::core::db::Database;
|
||||||
|
use momentry_core::core::time::FrameTime;
|
||||||
use momentry_core::ui::progress::{ProcessorType, ProgressState, ProgressUi};
|
use momentry_core::ui::progress::{ProcessorType, ProgressState, ProgressUi};
|
||||||
use momentry_core::{
|
use momentry_core::{
|
||||||
Embedder, OutputDir, PostgresDb, QdrantDb, RedisClient, VectorPayload, VideoRecord, VideoStatus,
|
Embedder, OutputDir, PostgresDb, QdrantDb, RedisClient, VectorPayload, VideoRecord, VideoStatus,
|
||||||
@@ -821,6 +823,7 @@ enum N8nAction {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
dotenv::dotenv().ok();
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
@@ -1808,16 +1811,14 @@ async fn main() -> Result<()> {
|
|||||||
// Store ASR sentence pre_chunks
|
// Store ASR sentence pre_chunks
|
||||||
let mut asr_pre_chunk_ids = Vec::new();
|
let mut asr_pre_chunk_ids = Vec::new();
|
||||||
for seg in asr_result.segments.iter() {
|
for seg in asr_result.segments.iter() {
|
||||||
let start_frame = (seg.start * fps) as i64;
|
let start_frame = FrameTime::from_seconds(seg.start, fps).frames();
|
||||||
let end_frame = (seg.end * fps) as i64;
|
let end_frame = FrameTime::from_seconds(seg.end, fps).frames();
|
||||||
let pre_chunk = momentry_core::core::db::postgres_db::PreChunk {
|
let pre_chunk = momentry_core::core::db::postgres_db::PreChunk {
|
||||||
id: 0,
|
id: 0,
|
||||||
file_id,
|
file_id,
|
||||||
source_type: "asr".to_string(),
|
source_type: "asr".to_string(),
|
||||||
source_file: Some(asr_path.clone()),
|
source_file: Some(asr_path.clone()),
|
||||||
chunk_type: "sentence".to_string(),
|
chunk_type: "sentence".to_string(),
|
||||||
start_time: seg.start,
|
|
||||||
end_time: seg.end,
|
|
||||||
start_frame,
|
start_frame,
|
||||||
end_frame,
|
end_frame,
|
||||||
fps,
|
fps,
|
||||||
@@ -1840,8 +1841,6 @@ async fn main() -> Result<()> {
|
|||||||
source_type: "cut".to_string(),
|
source_type: "cut".to_string(),
|
||||||
source_file: Some(cut_path.clone()),
|
source_file: Some(cut_path.clone()),
|
||||||
chunk_type: "cut".to_string(),
|
chunk_type: "cut".to_string(),
|
||||||
start_time: scene.start_time,
|
|
||||||
end_time: scene.end_time,
|
|
||||||
start_frame: scene.start_frame as i64,
|
start_frame: scene.start_frame as i64,
|
||||||
end_frame: scene.end_frame as i64,
|
end_frame: scene.end_frame as i64,
|
||||||
fps,
|
fps,
|
||||||
@@ -1863,8 +1862,8 @@ async fn main() -> Result<()> {
|
|||||||
let mut time_start = 0.0;
|
let mut time_start = 0.0;
|
||||||
while time_start < duration {
|
while time_start < duration {
|
||||||
let time_end = (time_start + 10.0).min(duration);
|
let time_end = (time_start + 10.0).min(duration);
|
||||||
let start_frame = (time_start * fps) as i64;
|
let start_frame = FrameTime::from_seconds(time_start, fps).frames();
|
||||||
let end_frame = (time_end * fps) as i64;
|
let end_frame = FrameTime::from_seconds(time_end, fps).frames();
|
||||||
|
|
||||||
let pre_chunk = momentry_core::core::db::postgres_db::PreChunk {
|
let pre_chunk = momentry_core::core::db::postgres_db::PreChunk {
|
||||||
id: 0,
|
id: 0,
|
||||||
@@ -1872,8 +1871,6 @@ async fn main() -> Result<()> {
|
|||||||
source_type: "time".to_string(),
|
source_type: "time".to_string(),
|
||||||
source_file: None,
|
source_file: None,
|
||||||
chunk_type: "time".to_string(),
|
chunk_type: "time".to_string(),
|
||||||
start_time: time_start,
|
|
||||||
end_time: time_end,
|
|
||||||
start_frame,
|
start_frame,
|
||||||
end_frame,
|
end_frame,
|
||||||
fps,
|
fps,
|
||||||
@@ -1965,7 +1962,7 @@ async fn main() -> Result<()> {
|
|||||||
let mut sentence_chunks = Vec::new();
|
let mut sentence_chunks = Vec::new();
|
||||||
for (i, seg) in asr_result.segments.iter().enumerate() {
|
for (i, seg) in asr_result.segments.iter().enumerate() {
|
||||||
let pre_chunk_id = asr_pre_chunk_ids.get(i).copied().unwrap_or(0);
|
let pre_chunk_id = asr_pre_chunk_ids.get(i).copied().unwrap_or(0);
|
||||||
let chunk = Chunk::new(
|
let chunk = Chunk::from_seconds(
|
||||||
file_id as i32,
|
file_id as i32,
|
||||||
uuid.clone(),
|
uuid.clone(),
|
||||||
i as u32,
|
i as u32,
|
||||||
@@ -1987,7 +1984,7 @@ async fn main() -> Result<()> {
|
|||||||
let mut cut_chunks = Vec::new();
|
let mut cut_chunks = Vec::new();
|
||||||
for (i, scene) in cut_result.scenes.iter().enumerate() {
|
for (i, scene) in cut_result.scenes.iter().enumerate() {
|
||||||
let pre_chunk_id = cut_pre_chunk_ids.get(i).copied().unwrap_or(0);
|
let pre_chunk_id = cut_pre_chunk_ids.get(i).copied().unwrap_or(0);
|
||||||
let chunk = Chunk::new(
|
let chunk = Chunk::from_seconds(
|
||||||
file_id as i32,
|
file_id as i32,
|
||||||
uuid.clone(),
|
uuid.clone(),
|
||||||
i as u32,
|
i as u32,
|
||||||
@@ -2016,8 +2013,8 @@ async fn main() -> Result<()> {
|
|||||||
i as u32,
|
i as u32,
|
||||||
ChunkType::TimeBased,
|
ChunkType::TimeBased,
|
||||||
ChunkRule::Rule1,
|
ChunkRule::Rule1,
|
||||||
tc.start_time,
|
tc.start_frame,
|
||||||
tc.end_time,
|
tc.end_frame,
|
||||||
fps,
|
fps,
|
||||||
serde_json::json!({"interval": 10.0}),
|
serde_json::json!({"interval": 10.0}),
|
||||||
)
|
)
|
||||||
@@ -2107,12 +2104,13 @@ async fn main() -> Result<()> {
|
|||||||
println!("\n=== Scene {} ===", i + 1);
|
println!("\n=== Scene {} ===", i + 1);
|
||||||
println!(
|
println!(
|
||||||
"Time: {:.2}s - {:.2}s",
|
"Time: {:.2}s - {:.2}s",
|
||||||
story_chunk.start_time, story_chunk.end_time
|
story_chunk.start_time().seconds(),
|
||||||
|
story_chunk.end_time().seconds()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get context: expand time range by 5 seconds before and after
|
// Get context: expand time range by 5 seconds before and after
|
||||||
let context_start = (story_chunk.start_time - 5.0).max(0.0);
|
let context_start = (story_chunk.start_time().seconds() - 5.0).max(0.0);
|
||||||
let context_end = (story_chunk.end_time + 5.0).min(duration);
|
let context_end = (story_chunk.end_time().seconds() + 5.0).min(duration);
|
||||||
|
|
||||||
// Get chunks in context range (sentence chunks with ASR text)
|
// Get chunks in context range (sentence chunks with ASR text)
|
||||||
let context_chunks = db
|
let context_chunks = db
|
||||||
@@ -2129,8 +2127,8 @@ async fn main() -> Result<()> {
|
|||||||
story.push_str(&format!(
|
story.push_str(&format!(
|
||||||
"Scene {} ({:.1}s - {:.1}s)\n\n",
|
"Scene {} ({:.1}s - {:.1}s)\n\n",
|
||||||
i + 1,
|
i + 1,
|
||||||
story_chunk.start_time,
|
story_chunk.start_time().seconds(),
|
||||||
story_chunk.end_time
|
story_chunk.end_time().seconds()
|
||||||
));
|
));
|
||||||
|
|
||||||
// Add audio/text content
|
// Add audio/text content
|
||||||
@@ -2229,18 +2227,24 @@ async fn main() -> Result<()> {
|
|||||||
.await
|
.await
|
||||||
.context("Failed to init PostgreSQL")?;
|
.context("Failed to init PostgreSQL")?;
|
||||||
let qdrant = QdrantDb::init().await.context("Failed to init Qdrant")?;
|
let qdrant = QdrantDb::init().await.context("Failed to init Qdrant")?;
|
||||||
let embedder = Embedder::new("nomic-embed-text:v1.5".to_string());
|
let embedder = Embedder::new("nomic-embed-text-v2-moe:latest".to_string());
|
||||||
|
|
||||||
let target_uuid = if uuid == "all" {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(uuid.as_str())
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut stored_count = 0usize;
|
let mut stored_count = 0usize;
|
||||||
|
|
||||||
if let Some(target) = target_uuid {
|
// Get list of videos to process
|
||||||
let chunks = pg.get_chunks_by_uuid(target).await?;
|
let videos_to_process = if uuid == "all" {
|
||||||
|
// Get all videos
|
||||||
|
let videos = pg.list_videos().await?;
|
||||||
|
videos.into_iter().map(|v| v.uuid).collect::<Vec<_>>()
|
||||||
|
} else {
|
||||||
|
// Process single video
|
||||||
|
vec![uuid.clone()]
|
||||||
|
};
|
||||||
|
|
||||||
|
for target in &videos_to_process {
|
||||||
|
println!("\n=== Processing video: {} ===", target);
|
||||||
|
|
||||||
|
let chunks = pg.get_chunks_by_uuid(target.as_str()).await?;
|
||||||
let sentence_chunks: Vec<_> = chunks
|
let sentence_chunks: Vec<_> = chunks
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|c| c.chunk_type == ChunkType::Sentence)
|
.filter(|c| c.chunk_type == ChunkType::Sentence)
|
||||||
@@ -2252,21 +2256,32 @@ async fn main() -> Result<()> {
|
|||||||
target
|
target
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut video_stored_count = 0usize;
|
||||||
|
|
||||||
for chunk in sentence_chunks {
|
for chunk in sentence_chunks {
|
||||||
|
// Try to extract text from different possible locations
|
||||||
let text = chunk
|
let text = chunk
|
||||||
.content
|
.content
|
||||||
.get("text")
|
.get("data") // Try data->text structure first
|
||||||
|
.and_then(|data| data.get("text"))
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
|
.or_else(|| chunk.content.get("text").and_then(|v| v.as_str())) // Try root text structure
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
|
|
||||||
if text.is_empty() {
|
if text.is_empty() {
|
||||||
|
eprintln!(
|
||||||
|
"Empty text for chunk {}, content: {:?}",
|
||||||
|
chunk.chunk_id, chunk.content
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
print!("Embedding chunk {}... ", chunk.chunk_id);
|
print!("Embedding chunk {}... ", chunk.chunk_id);
|
||||||
|
std::io::stdout().flush().unwrap();
|
||||||
|
|
||||||
match embedder.embed_document(text).await {
|
match embedder.embed_document(text).await {
|
||||||
Ok(vector) => {
|
Ok(vector) => {
|
||||||
|
println!("embedding success ({} dims)", vector.len());
|
||||||
let vector_id = format!("{}_{}", chunk.uuid, chunk.chunk_id);
|
let vector_id = format!("{}_{}", chunk.uuid, chunk.chunk_id);
|
||||||
|
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
@@ -2280,8 +2295,8 @@ async fn main() -> Result<()> {
|
|||||||
uuid: chunk.uuid.clone(),
|
uuid: chunk.uuid.clone(),
|
||||||
chunk_id: chunk.chunk_id.clone(),
|
chunk_id: chunk.chunk_id.clone(),
|
||||||
chunk_type: "sentence".to_string(),
|
chunk_type: "sentence".to_string(),
|
||||||
start_time: chunk.start_time,
|
start_time: chunk.start_time().seconds(),
|
||||||
end_time: chunk.end_time,
|
end_time: chunk.end_time().seconds(),
|
||||||
text: Some(text.to_string()),
|
text: Some(text.to_string()),
|
||||||
};
|
};
|
||||||
if let Err(e) = qdrant
|
if let Err(e) = qdrant
|
||||||
@@ -2298,32 +2313,40 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stored_count += 1;
|
stored_count += 1;
|
||||||
println!("done ({} dims)", vector.len());
|
video_stored_count += 1;
|
||||||
|
println!(
|
||||||
|
"stored (video: {}, total: {})",
|
||||||
|
video_stored_count, stored_count
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("failed: {}", e);
|
println!("embedding failed: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only update storage status if vectors were actually stored
|
// Only update storage status if vectors were actually stored for this video
|
||||||
if stored_count > 0 {
|
if video_stored_count > 0 {
|
||||||
pg.update_storage_status(target, "pvector_chunk", true)
|
pg.update_storage_status(target.as_str(), "pvector_chunk", true)
|
||||||
.await?;
|
.await?;
|
||||||
pg.update_storage_status(target, "qvector_chunk", true)
|
pg.update_storage_status(target.as_str(), "qvector_chunk", true)
|
||||||
.await?;
|
.await?;
|
||||||
println!(
|
println!(
|
||||||
"\n✓ Vectorize stage completed for {}! ({} vectors stored)",
|
"✓ Vectorize stage completed for {}! ({} vectors stored)",
|
||||||
target, stored_count
|
target, video_stored_count
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
"\n✗ Vectorize stage failed for {}! (0 vectors stored)",
|
"✗ Vectorize stage failed for {}! (0 vectors stored)",
|
||||||
target
|
target
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
println!("\n✓ Vectorize stage completed for all videos!");
|
|
||||||
|
println!("\n=== Vectorization Summary ===");
|
||||||
|
println!("Total vectors stored: {}", stored_count);
|
||||||
|
if uuid == "all" {
|
||||||
|
println!("✓ Vectorize stage completed for all videos!");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2408,13 +2431,16 @@ async fn main() -> Result<()> {
|
|||||||
} => {
|
} => {
|
||||||
use momentry_core::worker::{JobWorker, WorkerConfig};
|
use momentry_core::worker::{JobWorker, WorkerConfig};
|
||||||
|
|
||||||
let config = WorkerConfig {
|
let mut config = WorkerConfig::default();
|
||||||
max_concurrent: max_concurrent.unwrap_or(2),
|
if let Some(max) = max_concurrent {
|
||||||
poll_interval_secs: poll_interval.unwrap_or(5),
|
config.max_concurrent = max;
|
||||||
enabled: true,
|
}
|
||||||
batch_size: batch_size.unwrap_or(10),
|
if let Some(interval) = poll_interval {
|
||||||
processor_timeout_secs: 3600,
|
config.poll_interval_secs = interval;
|
||||||
};
|
}
|
||||||
|
if let Some(batch) = batch_size {
|
||||||
|
config.batch_size = batch;
|
||||||
|
}
|
||||||
|
|
||||||
let db = PostgresDb::init().await?;
|
let db = PostgresDb::init().await?;
|
||||||
let redis = RedisClient::new()?;
|
let redis = RedisClient::new()?;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const QDRANT_API_KEY: &str = "Test3200Test3200Test3200";
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
const OLLAMA_URL: &str = "http://localhost:11434";
|
const OLLAMA_URL: &str = "http://localhost:11434";
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
const MODEL: &str = "nomic-embed-text-v2-moe";
|
const MODEL: &str = "nomic-embed-text-v2-moe:latest";
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -112,8 +112,8 @@ impl ChunkSelector {
|
|||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search Qdrant - try both collections (chunks_v3 for multilingual, AccusysDB for others)
|
// Search Qdrant - use momentry_rule1 collection (Rule1 specification)
|
||||||
let collections = ["chunks_v3", "AccusysDB"];
|
let collections = ["momentry_rule1"];
|
||||||
|
|
||||||
for collection in collections {
|
for collection in collections {
|
||||||
let vector_str = serde_json::to_string(&embedding)
|
let vector_str = serde_json::to_string(&embedding)
|
||||||
|
|||||||
+20
-23
@@ -8,6 +8,7 @@ use std::sync::{Arc, Mutex};
|
|||||||
use momentry_core::core::api_key::{ApiKeyService, ApiKeyType};
|
use momentry_core::core::api_key::{ApiKeyService, ApiKeyType};
|
||||||
use momentry_core::core::chunk::types::{Chunk, ChunkRule, ChunkType};
|
use momentry_core::core::chunk::types::{Chunk, ChunkRule, ChunkType};
|
||||||
use momentry_core::core::db::Database;
|
use momentry_core::core::db::Database;
|
||||||
|
use momentry_core::core::time::FrameTime;
|
||||||
use momentry_core::ui::progress::{ProcessorType, ProgressState, ProgressUi};
|
use momentry_core::ui::progress::{ProcessorType, ProgressState, ProgressUi};
|
||||||
use momentry_core::{
|
use momentry_core::{
|
||||||
Embedder, OutputDir, PostgresDb, QdrantDb, RedisClient, VectorPayload, VideoRecord, VideoStatus,
|
Embedder, OutputDir, PostgresDb, QdrantDb, RedisClient, VectorPayload, VideoRecord, VideoStatus,
|
||||||
@@ -1818,16 +1819,14 @@ async fn main() -> Result<()> {
|
|||||||
// Store ASR sentence pre_chunks
|
// Store ASR sentence pre_chunks
|
||||||
let mut asr_pre_chunk_ids = Vec::new();
|
let mut asr_pre_chunk_ids = Vec::new();
|
||||||
for seg in asr_result.segments.iter() {
|
for seg in asr_result.segments.iter() {
|
||||||
let start_frame = (seg.start * fps) as i64;
|
let start_frame = FrameTime::from_seconds(seg.start, fps).frames();
|
||||||
let end_frame = (seg.end * fps) as i64;
|
let end_frame = FrameTime::from_seconds(seg.end, fps).frames();
|
||||||
let pre_chunk = momentry_core::core::db::postgres_db::PreChunk {
|
let pre_chunk = momentry_core::core::db::postgres_db::PreChunk {
|
||||||
id: 0,
|
id: 0,
|
||||||
file_id,
|
file_id,
|
||||||
source_type: "asr".to_string(),
|
source_type: "asr".to_string(),
|
||||||
source_file: Some(asr_path.clone()),
|
source_file: Some(asr_path.clone()),
|
||||||
chunk_type: "sentence".to_string(),
|
chunk_type: "sentence".to_string(),
|
||||||
start_time: seg.start,
|
|
||||||
end_time: seg.end,
|
|
||||||
start_frame,
|
start_frame,
|
||||||
end_frame,
|
end_frame,
|
||||||
fps,
|
fps,
|
||||||
@@ -1850,8 +1849,6 @@ async fn main() -> Result<()> {
|
|||||||
source_type: "cut".to_string(),
|
source_type: "cut".to_string(),
|
||||||
source_file: Some(cut_path.clone()),
|
source_file: Some(cut_path.clone()),
|
||||||
chunk_type: "cut".to_string(),
|
chunk_type: "cut".to_string(),
|
||||||
start_time: scene.start_time,
|
|
||||||
end_time: scene.end_time,
|
|
||||||
start_frame: scene.start_frame as i64,
|
start_frame: scene.start_frame as i64,
|
||||||
end_frame: scene.end_frame as i64,
|
end_frame: scene.end_frame as i64,
|
||||||
fps,
|
fps,
|
||||||
@@ -1873,8 +1870,8 @@ async fn main() -> Result<()> {
|
|||||||
let mut time_start = 0.0;
|
let mut time_start = 0.0;
|
||||||
while time_start < duration {
|
while time_start < duration {
|
||||||
let time_end = (time_start + 10.0).min(duration);
|
let time_end = (time_start + 10.0).min(duration);
|
||||||
let start_frame = (time_start * fps) as i64;
|
let start_frame = FrameTime::from_seconds(time_start, fps).frames();
|
||||||
let end_frame = (time_end * fps) as i64;
|
let end_frame = FrameTime::from_seconds(time_end, fps).frames();
|
||||||
|
|
||||||
let pre_chunk = momentry_core::core::db::postgres_db::PreChunk {
|
let pre_chunk = momentry_core::core::db::postgres_db::PreChunk {
|
||||||
id: 0,
|
id: 0,
|
||||||
@@ -1882,8 +1879,6 @@ async fn main() -> Result<()> {
|
|||||||
source_type: "time".to_string(),
|
source_type: "time".to_string(),
|
||||||
source_file: None,
|
source_file: None,
|
||||||
chunk_type: "time".to_string(),
|
chunk_type: "time".to_string(),
|
||||||
start_time: time_start,
|
|
||||||
end_time: time_end,
|
|
||||||
start_frame,
|
start_frame,
|
||||||
end_frame,
|
end_frame,
|
||||||
fps,
|
fps,
|
||||||
@@ -1975,7 +1970,7 @@ async fn main() -> Result<()> {
|
|||||||
let mut sentence_chunks = Vec::new();
|
let mut sentence_chunks = Vec::new();
|
||||||
for (i, seg) in asr_result.segments.iter().enumerate() {
|
for (i, seg) in asr_result.segments.iter().enumerate() {
|
||||||
let pre_chunk_id = asr_pre_chunk_ids.get(i).copied().unwrap_or(0);
|
let pre_chunk_id = asr_pre_chunk_ids.get(i).copied().unwrap_or(0);
|
||||||
let chunk = Chunk::new(
|
let chunk = Chunk::from_seconds(
|
||||||
file_id as i32,
|
file_id as i32,
|
||||||
uuid.clone(),
|
uuid.clone(),
|
||||||
i as u32,
|
i as u32,
|
||||||
@@ -1997,7 +1992,7 @@ async fn main() -> Result<()> {
|
|||||||
let mut cut_chunks = Vec::new();
|
let mut cut_chunks = Vec::new();
|
||||||
for (i, scene) in cut_result.scenes.iter().enumerate() {
|
for (i, scene) in cut_result.scenes.iter().enumerate() {
|
||||||
let pre_chunk_id = cut_pre_chunk_ids.get(i).copied().unwrap_or(0);
|
let pre_chunk_id = cut_pre_chunk_ids.get(i).copied().unwrap_or(0);
|
||||||
let chunk = Chunk::new(
|
let chunk = Chunk::from_seconds(
|
||||||
file_id as i32,
|
file_id as i32,
|
||||||
uuid.clone(),
|
uuid.clone(),
|
||||||
i as u32,
|
i as u32,
|
||||||
@@ -2026,8 +2021,8 @@ async fn main() -> Result<()> {
|
|||||||
i as u32,
|
i as u32,
|
||||||
ChunkType::TimeBased,
|
ChunkType::TimeBased,
|
||||||
ChunkRule::Rule1,
|
ChunkRule::Rule1,
|
||||||
tc.start_time,
|
tc.start_frame,
|
||||||
tc.end_time,
|
tc.end_frame,
|
||||||
fps,
|
fps,
|
||||||
serde_json::json!({"interval": 10.0}),
|
serde_json::json!({"interval": 10.0}),
|
||||||
)
|
)
|
||||||
@@ -2117,12 +2112,13 @@ async fn main() -> Result<()> {
|
|||||||
println!("\n=== Scene {} ===", i + 1);
|
println!("\n=== Scene {} ===", i + 1);
|
||||||
println!(
|
println!(
|
||||||
"Time: {:.2}s - {:.2}s",
|
"Time: {:.2}s - {:.2}s",
|
||||||
story_chunk.start_time, story_chunk.end_time
|
story_chunk.start_time().seconds(),
|
||||||
|
story_chunk.end_time().seconds()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get context: expand time range by 5 seconds before and after
|
// Get context: expand time range by 5 seconds before and after
|
||||||
let context_start = (story_chunk.start_time - 5.0).max(0.0);
|
let context_start = (story_chunk.start_time().seconds() - 5.0).max(0.0);
|
||||||
let context_end = (story_chunk.end_time + 5.0).min(duration);
|
let context_end = (story_chunk.end_time().seconds() + 5.0).min(duration);
|
||||||
|
|
||||||
// Get chunks in context range (sentence chunks with ASR text)
|
// Get chunks in context range (sentence chunks with ASR text)
|
||||||
let context_chunks = db
|
let context_chunks = db
|
||||||
@@ -2139,8 +2135,8 @@ async fn main() -> Result<()> {
|
|||||||
story.push_str(&format!(
|
story.push_str(&format!(
|
||||||
"Scene {} ({:.1}s - {:.1}s)\n\n",
|
"Scene {} ({:.1}s - {:.1}s)\n\n",
|
||||||
i + 1,
|
i + 1,
|
||||||
story_chunk.start_time,
|
story_chunk.start_time().seconds(),
|
||||||
story_chunk.end_time
|
story_chunk.end_time().seconds()
|
||||||
));
|
));
|
||||||
|
|
||||||
// Add audio/text content
|
// Add audio/text content
|
||||||
@@ -2239,7 +2235,7 @@ async fn main() -> Result<()> {
|
|||||||
.await
|
.await
|
||||||
.context("Failed to init PostgreSQL")?;
|
.context("Failed to init PostgreSQL")?;
|
||||||
let qdrant = QdrantDb::init().await.context("Failed to init Qdrant")?;
|
let qdrant = QdrantDb::init().await.context("Failed to init Qdrant")?;
|
||||||
let embedder = Embedder::new("nomic-embed-text:v1.5".to_string());
|
let embedder = Embedder::new("nomic-embed-text-v2-moe:latest".to_string());
|
||||||
|
|
||||||
let target_uuid = if uuid == "all" {
|
let target_uuid = if uuid == "all" {
|
||||||
None
|
None
|
||||||
@@ -2265,7 +2261,8 @@ async fn main() -> Result<()> {
|
|||||||
for chunk in sentence_chunks {
|
for chunk in sentence_chunks {
|
||||||
let text = chunk
|
let text = chunk
|
||||||
.content
|
.content
|
||||||
.get("text")
|
.get("data")
|
||||||
|
.and_then(|data| data.get("text"))
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
|
|
||||||
@@ -2290,8 +2287,8 @@ async fn main() -> Result<()> {
|
|||||||
uuid: chunk.uuid.clone(),
|
uuid: chunk.uuid.clone(),
|
||||||
chunk_id: chunk.chunk_id.clone(),
|
chunk_id: chunk.chunk_id.clone(),
|
||||||
chunk_type: "sentence".to_string(),
|
chunk_type: "sentence".to_string(),
|
||||||
start_time: chunk.start_time,
|
start_time: chunk.start_time().seconds(),
|
||||||
end_time: chunk.end_time,
|
end_time: chunk.end_time().seconds(),
|
||||||
text: Some(text.to_string()),
|
text: Some(text.to_string()),
|
||||||
};
|
};
|
||||||
if let Err(e) = qdrant
|
if let Err(e) = qdrant
|
||||||
|
|||||||
+102
-13
@@ -1,10 +1,13 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use crate::core::db::{MonitorJobStatus, PostgresDb, ProcessorType, RedisClient};
|
use crate::core::db::{
|
||||||
|
MonitorJobStatus, PostgresDb, ProcessorJobStatus, ProcessorType, RedisClient, VideoStatus,
|
||||||
|
};
|
||||||
use crate::worker::config::WorkerConfig;
|
use crate::worker::config::WorkerConfig;
|
||||||
use crate::worker::processor::{ProcessorPool, ProcessorTask};
|
use crate::worker::processor::{ProcessorPool, ProcessorTask};
|
||||||
|
|
||||||
@@ -49,22 +52,33 @@ impl JobWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn poll_and_process(&self) -> Result<()> {
|
async fn poll_and_process(&self) -> Result<()> {
|
||||||
let pending_jobs = self.db.get_pending_jobs(self.config.batch_size).await?;
|
// Always check for completion of running jobs first
|
||||||
|
// This ensures jobs with all processors in terminal states are marked complete/failed
|
||||||
if pending_jobs.is_empty() {
|
let running_jobs_done = self
|
||||||
return Ok(());
|
.db
|
||||||
|
.get_running_jobs_with_all_processors_done(self.config.batch_size)
|
||||||
|
.await?;
|
||||||
|
for job in running_jobs_done {
|
||||||
|
if let Err(e) = self.check_and_complete_job(job.id, &job.uuid).await {
|
||||||
|
error!("Failed to complete job {}: {}", job.uuid, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Found {} pending jobs", pending_jobs.len());
|
// Process pending jobs if any
|
||||||
|
let pending_jobs = self.db.get_pending_jobs(self.config.batch_size).await?;
|
||||||
|
|
||||||
for job in pending_jobs {
|
if !pending_jobs.is_empty() {
|
||||||
if !self.processor_pool.can_start().await {
|
info!("Found {} pending jobs", pending_jobs.len());
|
||||||
info!("Max concurrent processors reached, waiting...");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = self.process_job(job).await {
|
for job in pending_jobs {
|
||||||
error!("Failed to process job: {}", e);
|
if !self.processor_pool.can_start().await {
|
||||||
|
info!("Max concurrent processors reached, waiting...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = self.process_job(job).await {
|
||||||
|
error!("Failed to process job: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +98,50 @@ impl JobWorker {
|
|||||||
.update_worker_job_status(&job.uuid, job.id, "running", None, 0, total_processors)
|
.update_worker_job_status(&job.uuid, job.id, "running", None, 0, total_processors)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Get existing processor results for this job
|
||||||
|
let existing_results = self.db.get_processor_results_by_job(job.id).await?;
|
||||||
|
let mut result_map = HashMap::new();
|
||||||
|
for result in existing_results {
|
||||||
|
result_map.insert(result.processor_type, result);
|
||||||
|
}
|
||||||
|
|
||||||
for processor_type in ProcessorType::all() {
|
for processor_type in ProcessorType::all() {
|
||||||
|
// Check if processor already in terminal state
|
||||||
|
if let Some(result) = result_map.get(&processor_type) {
|
||||||
|
match result.status {
|
||||||
|
ProcessorJobStatus::Completed | ProcessorJobStatus::Skipped => {
|
||||||
|
info!(
|
||||||
|
"Processor {} already completed, skipping",
|
||||||
|
processor_type.as_str()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ProcessorJobStatus::Failed => {
|
||||||
|
info!("Processor {} failed, skipping", processor_type.as_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ProcessorJobStatus::Running => {
|
||||||
|
info!(
|
||||||
|
"Processor {} already running, skipping",
|
||||||
|
processor_type.as_str()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ProcessorJobStatus::Pending => {
|
||||||
|
// Continue to start processor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check capacity before starting processor
|
||||||
|
if !self.processor_pool.can_start().await {
|
||||||
|
info!(
|
||||||
|
"Max concurrent processors reached, skipping remaining processors for job {}",
|
||||||
|
job.uuid
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
let processor_result_id = self
|
let processor_result_id = self
|
||||||
.db
|
.db
|
||||||
.create_processor_result(job.id, processor_type)
|
.create_processor_result(job.id, processor_type)
|
||||||
@@ -134,11 +191,39 @@ impl JobWorker {
|
|||||||
})
|
})
|
||||||
.count() as i32;
|
.count() as i32;
|
||||||
|
|
||||||
|
// Compute completed and failed processor arrays
|
||||||
|
let completed_processors: Vec<String> = results
|
||||||
|
.iter()
|
||||||
|
.filter(|r| {
|
||||||
|
matches!(
|
||||||
|
r.status,
|
||||||
|
crate::core::db::ProcessorJobStatus::Completed
|
||||||
|
| crate::core::db::ProcessorJobStatus::Skipped
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|r| r.processor_type.as_str().to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let failed_processors: Vec<String> = results
|
||||||
|
.iter()
|
||||||
|
.filter(|r| matches!(r.status, crate::core::db::ProcessorJobStatus::Failed))
|
||||||
|
.map(|r| r.processor_type.as_str().to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Update processor arrays in job record
|
||||||
|
self.db
|
||||||
|
.update_job_processors_arrays(job_id, completed_processors, failed_processors)
|
||||||
|
.await?;
|
||||||
|
|
||||||
if all_completed && !any_failed {
|
if all_completed && !any_failed {
|
||||||
self.db
|
self.db
|
||||||
.update_job_status(job_id, MonitorJobStatus::Completed)
|
.update_job_status(job_id, MonitorJobStatus::Completed)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
self.db
|
||||||
|
.update_video_status(uuid, VideoStatus::Completed)
|
||||||
|
.await?;
|
||||||
|
|
||||||
self.redis
|
self.redis
|
||||||
.update_worker_job_status(uuid, job_id, "completed", None, completed_count, 7)
|
.update_worker_job_status(uuid, job_id, "completed", None, completed_count, 7)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -151,6 +236,10 @@ impl JobWorker {
|
|||||||
.update_job_status(job_id, MonitorJobStatus::Failed)
|
.update_job_status(job_id, MonitorJobStatus::Failed)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
self.db
|
||||||
|
.update_video_status(uuid, VideoStatus::Failed)
|
||||||
|
.await?;
|
||||||
|
|
||||||
self.redis
|
self.redis
|
||||||
.update_worker_job_status(uuid, job_id, "failed", None, completed_count, 7)
|
.update_worker_job_status(uuid, job_id, "failed", None, completed_count, 7)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
+529
-31
@@ -1,11 +1,22 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{mpsc, RwLock};
|
use tokio::sync::{mpsc, RwLock};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
use crate::core::chunk::types::{Chunk, ChunkRule, ChunkType};
|
||||||
|
use crate::core::config::{OUTPUT_DIR, PYTHON_PATH, SCRIPTS_DIR};
|
||||||
use crate::core::db::RedisClient;
|
use crate::core::db::RedisClient;
|
||||||
use crate::core::db::{MonitorJob, PostgresDb, ProcessorJobStatus, ProcessorType};
|
use crate::core::db::{MonitorJob, PostgresDb, ProcessorJobStatus, ProcessorType};
|
||||||
|
use crate::core::processor;
|
||||||
|
use crate::core::processor::asr::AsrResult;
|
||||||
|
use crate::core::processor::asrx::AsrxResult;
|
||||||
|
use crate::core::processor::cut::CutResult;
|
||||||
|
use crate::core::processor::face::FaceResult;
|
||||||
|
use crate::core::processor::ocr::OcrResult;
|
||||||
|
use crate::core::processor::pose::PoseResult;
|
||||||
|
use crate::core::processor::yolo::YoloResult;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ProcessorTask {
|
pub struct ProcessorTask {
|
||||||
@@ -104,46 +115,58 @@ impl ProcessorPool {
|
|||||||
"Processor {} completed for job {}",
|
"Processor {} completed for job {}",
|
||||||
processor_name, job.uuid
|
processor_name, job.uuid
|
||||||
);
|
);
|
||||||
let _ = db
|
if let Err(e) = db
|
||||||
.update_processor_result(
|
.update_processor_result(
|
||||||
processor_result_id,
|
processor_result_id,
|
||||||
ProcessorJobStatus::Completed,
|
ProcessorJobStatus::Completed,
|
||||||
None,
|
None,
|
||||||
Some(&output),
|
Some(&output),
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
{
|
||||||
|
error!("Failed to update processor result to completed: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
let _ = redis
|
if let Err(e) = redis
|
||||||
.update_worker_processor_status(
|
.update_worker_processor_status(
|
||||||
&job.uuid,
|
&job.uuid,
|
||||||
&processor_name,
|
&processor_name,
|
||||||
"completed",
|
"completed",
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
{
|
||||||
|
error!("Failed to update Redis processor status: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
"Processor {} failed for job {}: {}",
|
"Processor {} failed for job {}: {}",
|
||||||
processor_name, job.uuid, e
|
processor_name, job.uuid, e
|
||||||
);
|
);
|
||||||
let _ = db
|
if let Err(db_err) = db
|
||||||
.update_processor_result(
|
.update_processor_result(
|
||||||
processor_result_id,
|
processor_result_id,
|
||||||
ProcessorJobStatus::Failed,
|
ProcessorJobStatus::Failed,
|
||||||
Some(&e.to_string()),
|
Some(&e.to_string()),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
{
|
||||||
|
error!("Failed to update processor result to failed: {}", db_err);
|
||||||
|
}
|
||||||
|
|
||||||
let _ = redis
|
if let Err(redis_err) = redis
|
||||||
.update_worker_processor_status(
|
.update_worker_processor_status(
|
||||||
&job.uuid,
|
&job.uuid,
|
||||||
&processor_name,
|
&processor_name,
|
||||||
"failed",
|
"failed",
|
||||||
Some(&e.to_string()),
|
Some(&e.to_string()),
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
{
|
||||||
|
error!("Failed to update Redis processor status: {}", redis_err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -153,24 +176,136 @@ impl ProcessorPool {
|
|||||||
|
|
||||||
async fn run_processor(
|
async fn run_processor(
|
||||||
db: &PostgresDb,
|
db: &PostgresDb,
|
||||||
redis: &RedisClient,
|
_redis: &RedisClient,
|
||||||
job: &MonitorJob,
|
job: &MonitorJob,
|
||||||
processor_type: ProcessorType,
|
processor_type: ProcessorType,
|
||||||
mut cancel_rx: mpsc::Receiver<()>,
|
_cancel_rx: mpsc::Receiver<()>,
|
||||||
) -> Result<serde_json::Value> {
|
) -> Result<serde_json::Value> {
|
||||||
let video_path = job.video_path.as_ref().context("No video path in job")?;
|
let video_path = job.video_path.as_ref().context("No video path in job")?;
|
||||||
|
|
||||||
|
// Generate output path
|
||||||
|
let output_dir = PathBuf::from(OUTPUT_DIR.as_str());
|
||||||
|
let output_path = output_dir.join(format!(
|
||||||
|
"job_{}_{}_{}.json",
|
||||||
|
job.id,
|
||||||
|
processor_type.as_str(),
|
||||||
|
chrono::Utc::now().timestamp_millis()
|
||||||
|
));
|
||||||
|
|
||||||
|
// Ensure output directory exists
|
||||||
|
if let Some(parent) = output_path.parent() {
|
||||||
|
tokio::fs::create_dir_all(parent).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let uuid = Some(job.uuid.as_str());
|
||||||
|
|
||||||
match processor_type {
|
match processor_type {
|
||||||
ProcessorType::Asr => Self::run_asr(db, redis, video_path, &mut cancel_rx).await,
|
ProcessorType::Asr => {
|
||||||
ProcessorType::Cut => Self::run_cut(db, redis, video_path, &mut cancel_rx).await,
|
let result =
|
||||||
ProcessorType::Yolo => Self::run_yolo(db, redis, video_path, &mut cancel_rx).await,
|
processor::process_asr(video_path, output_path.to_str().unwrap(), uuid).await?;
|
||||||
ProcessorType::Ocr => Self::run_ocr(db, redis, video_path, &mut cancel_rx).await,
|
// Store ASR chunks in database
|
||||||
ProcessorType::Face => Self::run_face(db, redis, video_path, &mut cancel_rx).await,
|
tracing::info!(
|
||||||
ProcessorType::Pose => Self::run_pose(db, redis, video_path, &mut cancel_rx).await,
|
"ASR completed, storing {} segments for {}",
|
||||||
ProcessorType::Asrx => Self::run_asrx(db, redis, video_path, &mut cancel_rx).await,
|
result.segments.len(),
|
||||||
|
job.uuid
|
||||||
|
);
|
||||||
|
if let Err(e) = Self::store_asr_chunks(db, &job.uuid, &result).await {
|
||||||
|
tracing::error!("Failed to store ASR chunks for {}: {}", job.uuid, e);
|
||||||
|
}
|
||||||
|
Ok(serde_json::to_value(result)?)
|
||||||
|
}
|
||||||
|
ProcessorType::Cut => {
|
||||||
|
let result =
|
||||||
|
processor::process_cut(video_path, output_path.to_str().unwrap(), uuid).await?;
|
||||||
|
// Store CUT chunks in database
|
||||||
|
tracing::info!(
|
||||||
|
"CUT completed, storing {} scenes for {}",
|
||||||
|
result.scenes.len(),
|
||||||
|
job.uuid
|
||||||
|
);
|
||||||
|
if let Err(e) = Self::store_cut_chunks(db, &job.uuid, &result).await {
|
||||||
|
tracing::error!("Failed to store CUT chunks for {}: {}", job.uuid, e);
|
||||||
|
}
|
||||||
|
Ok(serde_json::to_value(result)?)
|
||||||
|
}
|
||||||
|
ProcessorType::Yolo => {
|
||||||
|
let result =
|
||||||
|
processor::process_yolo(video_path, output_path.to_str().unwrap(), uuid)
|
||||||
|
.await?;
|
||||||
|
// Store YOLO chunks in database
|
||||||
|
tracing::info!(
|
||||||
|
"YOLO completed, storing {} frames for {}",
|
||||||
|
result.frames.len(),
|
||||||
|
job.uuid
|
||||||
|
);
|
||||||
|
if let Err(e) = Self::store_yolo_chunks(db, &job.uuid, &result).await {
|
||||||
|
tracing::error!("Failed to store YOLO chunks for {}: {}", job.uuid, e);
|
||||||
|
}
|
||||||
|
Ok(serde_json::to_value(result)?)
|
||||||
|
}
|
||||||
|
ProcessorType::Ocr => {
|
||||||
|
let result =
|
||||||
|
processor::process_ocr(video_path, output_path.to_str().unwrap(), uuid).await?;
|
||||||
|
// Store OCR chunks in database
|
||||||
|
tracing::info!(
|
||||||
|
"OCR completed, storing {} frames for {}",
|
||||||
|
result.frames.len(),
|
||||||
|
job.uuid
|
||||||
|
);
|
||||||
|
if let Err(e) = Self::store_ocr_chunks(db, &job.uuid, &result).await {
|
||||||
|
tracing::error!("Failed to store OCR chunks for {}: {}", job.uuid, e);
|
||||||
|
}
|
||||||
|
Ok(serde_json::to_value(result)?)
|
||||||
|
}
|
||||||
|
ProcessorType::Face => {
|
||||||
|
let result =
|
||||||
|
processor::process_face(video_path, output_path.to_str().unwrap(), uuid)
|
||||||
|
.await?;
|
||||||
|
// Store FACE chunks in database
|
||||||
|
tracing::info!(
|
||||||
|
"FACE completed, storing {} frames for {}",
|
||||||
|
result.frames.len(),
|
||||||
|
job.uuid
|
||||||
|
);
|
||||||
|
if let Err(e) = Self::store_face_chunks(db, &job.uuid, &result).await {
|
||||||
|
tracing::error!("Failed to store FACE chunks for {}: {}", job.uuid, e);
|
||||||
|
}
|
||||||
|
Ok(serde_json::to_value(result)?)
|
||||||
|
}
|
||||||
|
ProcessorType::Pose => {
|
||||||
|
let result =
|
||||||
|
processor::process_pose(video_path, output_path.to_str().unwrap(), uuid)
|
||||||
|
.await?;
|
||||||
|
// Store POSE chunks in database
|
||||||
|
tracing::info!(
|
||||||
|
"POSE completed, storing {} frames for {}",
|
||||||
|
result.frames.len(),
|
||||||
|
job.uuid
|
||||||
|
);
|
||||||
|
if let Err(e) = Self::store_pose_chunks(db, &job.uuid, &result).await {
|
||||||
|
tracing::error!("Failed to store POSE chunks for {}: {}", job.uuid, e);
|
||||||
|
}
|
||||||
|
Ok(serde_json::to_value(result)?)
|
||||||
|
}
|
||||||
|
ProcessorType::Asrx => {
|
||||||
|
let result =
|
||||||
|
processor::process_asrx(video_path, output_path.to_str().unwrap(), uuid)
|
||||||
|
.await?;
|
||||||
|
// Store ASRX chunks in database
|
||||||
|
tracing::info!(
|
||||||
|
"ASRX completed, storing {} segments for {}",
|
||||||
|
result.segments.len(),
|
||||||
|
job.uuid
|
||||||
|
);
|
||||||
|
if let Err(e) = Self::store_asrx_chunks(db, &job.uuid, &result).await {
|
||||||
|
tracing::error!("Failed to store ASRX chunks for {}: {}", job.uuid, e);
|
||||||
|
}
|
||||||
|
Ok(serde_json::to_value(result)?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
async fn run_asr(
|
async fn run_asr(
|
||||||
_db: &PostgresDb,
|
_db: &PostgresDb,
|
||||||
_redis: &RedisClient,
|
_redis: &RedisClient,
|
||||||
@@ -178,9 +313,9 @@ impl ProcessorPool {
|
|||||||
_cancel_rx: &mut mpsc::Receiver<()>,
|
_cancel_rx: &mut mpsc::Receiver<()>,
|
||||||
) -> Result<serde_json::Value> {
|
) -> Result<serde_json::Value> {
|
||||||
let script_path = std::env::var("MOMENTRY_ASR_SCRIPT")
|
let script_path = std::env::var("MOMENTRY_ASR_SCRIPT")
|
||||||
.unwrap_or_else(|_| "/Users/accusys/momentry/scripts/asr.py".to_string());
|
.unwrap_or_else(|_| format!("{}/asr_processor.py", SCRIPTS_DIR.as_str()));
|
||||||
|
|
||||||
let output = tokio::process::Command::new("/opt/homebrew/bin/python3.11")
|
let output = tokio::process::Command::new(PYTHON_PATH.as_str())
|
||||||
.arg(&script_path)
|
.arg(&script_path)
|
||||||
.arg(video_path)
|
.arg(video_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -195,6 +330,7 @@ impl ProcessorPool {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
async fn run_cut(
|
async fn run_cut(
|
||||||
_db: &PostgresDb,
|
_db: &PostgresDb,
|
||||||
_redis: &RedisClient,
|
_redis: &RedisClient,
|
||||||
@@ -202,9 +338,9 @@ impl ProcessorPool {
|
|||||||
_cancel_rx: &mut mpsc::Receiver<()>,
|
_cancel_rx: &mut mpsc::Receiver<()>,
|
||||||
) -> Result<serde_json::Value> {
|
) -> Result<serde_json::Value> {
|
||||||
let script_path = std::env::var("MOMENTRY_CUT_SCRIPT")
|
let script_path = std::env::var("MOMENTRY_CUT_SCRIPT")
|
||||||
.unwrap_or_else(|_| "/Users/accusys/momentry/scripts/cut.py".to_string());
|
.unwrap_or_else(|_| format!("{}/cut_processor.py", SCRIPTS_DIR.as_str()));
|
||||||
|
|
||||||
let output = tokio::process::Command::new("/opt/homebrew/bin/python3.11")
|
let output = tokio::process::Command::new(PYTHON_PATH.as_str())
|
||||||
.arg(&script_path)
|
.arg(&script_path)
|
||||||
.arg(video_path)
|
.arg(video_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -219,6 +355,7 @@ impl ProcessorPool {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
async fn run_yolo(
|
async fn run_yolo(
|
||||||
_db: &PostgresDb,
|
_db: &PostgresDb,
|
||||||
_redis: &RedisClient,
|
_redis: &RedisClient,
|
||||||
@@ -226,9 +363,9 @@ impl ProcessorPool {
|
|||||||
_cancel_rx: &mut mpsc::Receiver<()>,
|
_cancel_rx: &mut mpsc::Receiver<()>,
|
||||||
) -> Result<serde_json::Value> {
|
) -> Result<serde_json::Value> {
|
||||||
let script_path = std::env::var("MOMENTRY_YOLO_SCRIPT")
|
let script_path = std::env::var("MOMENTRY_YOLO_SCRIPT")
|
||||||
.unwrap_or_else(|_| "/Users/accusys/momentry/scripts/yolo_processor.py".to_string());
|
.unwrap_or_else(|_| format!("{}/yolo_processor.py", SCRIPTS_DIR.as_str()));
|
||||||
|
|
||||||
let output = tokio::process::Command::new("/opt/homebrew/bin/python3.11")
|
let output = tokio::process::Command::new(PYTHON_PATH.as_str())
|
||||||
.arg(&script_path)
|
.arg(&script_path)
|
||||||
.arg(video_path)
|
.arg(video_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -243,6 +380,7 @@ impl ProcessorPool {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
async fn run_ocr(
|
async fn run_ocr(
|
||||||
_db: &PostgresDb,
|
_db: &PostgresDb,
|
||||||
_redis: &RedisClient,
|
_redis: &RedisClient,
|
||||||
@@ -250,9 +388,9 @@ impl ProcessorPool {
|
|||||||
_cancel_rx: &mut mpsc::Receiver<()>,
|
_cancel_rx: &mut mpsc::Receiver<()>,
|
||||||
) -> Result<serde_json::Value> {
|
) -> Result<serde_json::Value> {
|
||||||
let script_path = std::env::var("MOMENTRY_OCR_SCRIPT")
|
let script_path = std::env::var("MOMENTRY_OCR_SCRIPT")
|
||||||
.unwrap_or_else(|_| "/Users/accusys/momentry/scripts/ocr.py".to_string());
|
.unwrap_or_else(|_| format!("{}/ocr_processor.py", SCRIPTS_DIR.as_str()));
|
||||||
|
|
||||||
let output = tokio::process::Command::new("/opt/homebrew/bin/python3.11")
|
let output = tokio::process::Command::new(PYTHON_PATH.as_str())
|
||||||
.arg(&script_path)
|
.arg(&script_path)
|
||||||
.arg(video_path)
|
.arg(video_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -267,6 +405,7 @@ impl ProcessorPool {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
async fn run_face(
|
async fn run_face(
|
||||||
_db: &PostgresDb,
|
_db: &PostgresDb,
|
||||||
_redis: &RedisClient,
|
_redis: &RedisClient,
|
||||||
@@ -274,9 +413,9 @@ impl ProcessorPool {
|
|||||||
_cancel_rx: &mut mpsc::Receiver<()>,
|
_cancel_rx: &mut mpsc::Receiver<()>,
|
||||||
) -> Result<serde_json::Value> {
|
) -> Result<serde_json::Value> {
|
||||||
let script_path = std::env::var("MOMENTRY_FACE_SCRIPT")
|
let script_path = std::env::var("MOMENTRY_FACE_SCRIPT")
|
||||||
.unwrap_or_else(|_| "/Users/accusys/momentry/scripts/face.py".to_string());
|
.unwrap_or_else(|_| format!("{}/face_processor.py", SCRIPTS_DIR.as_str()));
|
||||||
|
|
||||||
let output = tokio::process::Command::new("/opt/homebrew/bin/python3.11")
|
let output = tokio::process::Command::new(PYTHON_PATH.as_str())
|
||||||
.arg(&script_path)
|
.arg(&script_path)
|
||||||
.arg(video_path)
|
.arg(video_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -291,6 +430,7 @@ impl ProcessorPool {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
async fn run_pose(
|
async fn run_pose(
|
||||||
_db: &PostgresDb,
|
_db: &PostgresDb,
|
||||||
_redis: &RedisClient,
|
_redis: &RedisClient,
|
||||||
@@ -298,9 +438,9 @@ impl ProcessorPool {
|
|||||||
_cancel_rx: &mut mpsc::Receiver<()>,
|
_cancel_rx: &mut mpsc::Receiver<()>,
|
||||||
) -> Result<serde_json::Value> {
|
) -> Result<serde_json::Value> {
|
||||||
let script_path = std::env::var("MOMENTRY_POSE_SCRIPT")
|
let script_path = std::env::var("MOMENTRY_POSE_SCRIPT")
|
||||||
.unwrap_or_else(|_| "/Users/accusys/momentry/scripts/pose.py".to_string());
|
.unwrap_or_else(|_| format!("{}/pose_processor.py", SCRIPTS_DIR.as_str()));
|
||||||
|
|
||||||
let output = tokio::process::Command::new("/opt/homebrew/bin/python3.11")
|
let output = tokio::process::Command::new(PYTHON_PATH.as_str())
|
||||||
.arg(&script_path)
|
.arg(&script_path)
|
||||||
.arg(video_path)
|
.arg(video_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -315,6 +455,7 @@ impl ProcessorPool {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
async fn run_asrx(
|
async fn run_asrx(
|
||||||
_db: &PostgresDb,
|
_db: &PostgresDb,
|
||||||
_redis: &RedisClient,
|
_redis: &RedisClient,
|
||||||
@@ -322,9 +463,9 @@ impl ProcessorPool {
|
|||||||
_cancel_rx: &mut mpsc::Receiver<()>,
|
_cancel_rx: &mut mpsc::Receiver<()>,
|
||||||
) -> Result<serde_json::Value> {
|
) -> Result<serde_json::Value> {
|
||||||
let script_path = std::env::var("MOMENTRY_ASRX_SCRIPT")
|
let script_path = std::env::var("MOMENTRY_ASRX_SCRIPT")
|
||||||
.unwrap_or_else(|_| "/Users/accusys/momentry/scripts/asrx.py".to_string());
|
.unwrap_or_else(|_| format!("{}/asrx_processor.py", SCRIPTS_DIR.as_str()));
|
||||||
|
|
||||||
let output = tokio::process::Command::new("/opt/homebrew/bin/python3.11")
|
let output = tokio::process::Command::new(PYTHON_PATH.as_str())
|
||||||
.arg(&script_path)
|
.arg(&script_path)
|
||||||
.arg(video_path)
|
.arg(video_path)
|
||||||
.output()
|
.output()
|
||||||
@@ -339,6 +480,363 @@ impl ProcessorPool {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn store_asr_chunks(
|
||||||
|
db: &PostgresDb,
|
||||||
|
uuid: &str,
|
||||||
|
asr_result: &AsrResult,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Get video record to obtain file_id and fps
|
||||||
|
let video = db
|
||||||
|
.get_video_by_uuid(uuid)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Video not found for uuid: {}", uuid))?;
|
||||||
|
let file_id = video.id;
|
||||||
|
let fps = if video.fps > 0.0 { video.fps } else { 30.0 };
|
||||||
|
|
||||||
|
for (i, segment) in asr_result.segments.iter().enumerate() {
|
||||||
|
let chunk = Chunk::from_seconds(
|
||||||
|
file_id as i32,
|
||||||
|
uuid.to_string(),
|
||||||
|
i as u32,
|
||||||
|
ChunkType::Sentence,
|
||||||
|
ChunkRule::Rule1,
|
||||||
|
segment.start,
|
||||||
|
segment.end,
|
||||||
|
fps,
|
||||||
|
serde_json::json!({
|
||||||
|
"text": segment.text,
|
||||||
|
"text_normalized": segment.text.to_lowercase(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.with_metadata(serde_json::json!({
|
||||||
|
"language": asr_result.language,
|
||||||
|
"language_probability": asr_result.language_probability,
|
||||||
|
}));
|
||||||
|
|
||||||
|
match db.store_chunk(&chunk).await {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::info!("Stored ASR chunk {} for video {}", i, uuid);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to store ASR chunk {}: {}", i, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn store_cut_chunks(
|
||||||
|
db: &PostgresDb,
|
||||||
|
uuid: &str,
|
||||||
|
cut_result: &CutResult,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Get video record to obtain file_id and fps
|
||||||
|
let video = db
|
||||||
|
.get_video_by_uuid(uuid)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Video not found for uuid: {}", uuid))?;
|
||||||
|
let file_id = video.id;
|
||||||
|
let fps = if video.fps > 0.0 { video.fps } else { 30.0 };
|
||||||
|
|
||||||
|
for (i, scene) in cut_result.scenes.iter().enumerate() {
|
||||||
|
let chunk = Chunk::from_seconds(
|
||||||
|
file_id as i32,
|
||||||
|
uuid.to_string(),
|
||||||
|
i as u32,
|
||||||
|
ChunkType::Cut,
|
||||||
|
ChunkRule::Rule1,
|
||||||
|
scene.start_time,
|
||||||
|
scene.end_time,
|
||||||
|
fps,
|
||||||
|
serde_json::json!({
|
||||||
|
"scene_number": scene.scene_number,
|
||||||
|
"start_frame": scene.start_frame,
|
||||||
|
"end_frame": scene.end_frame,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
match db.store_chunk(&chunk).await {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::info!("Stored CUT chunk {} for video {}", i, uuid);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to store CUT chunk {}: {}", i, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn store_yolo_chunks(
|
||||||
|
db: &PostgresDb,
|
||||||
|
uuid: &str,
|
||||||
|
yolo_result: &YoloResult,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Get video record to obtain file_id and fps
|
||||||
|
let video = match db.get_video_by_uuid(uuid).await {
|
||||||
|
Ok(Some(video)) => video,
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::error!("Video not found for uuid: {}", uuid);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to get video for uuid {}: {}", uuid, e);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let file_id = video.id;
|
||||||
|
let fps = if video.fps > 0.0 { video.fps } else { 30.0 };
|
||||||
|
|
||||||
|
for (i, frame) in yolo_result.frames.iter().enumerate() {
|
||||||
|
let mut chunk = Chunk::new(
|
||||||
|
file_id as i32,
|
||||||
|
uuid.to_string(),
|
||||||
|
i as u32,
|
||||||
|
ChunkType::Trace,
|
||||||
|
ChunkRule::Rule1,
|
||||||
|
frame.frame as i64,
|
||||||
|
frame.frame as i64 + 1,
|
||||||
|
fps,
|
||||||
|
serde_json::json!({
|
||||||
|
"objects": frame.objects,
|
||||||
|
"timestamp": frame.timestamp,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Override chunk_id to include processor prefix for uniqueness
|
||||||
|
chunk.chunk_id = format!("trace_yolo_{:04}", i);
|
||||||
|
|
||||||
|
match db.store_chunk(&chunk).await {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::info!(
|
||||||
|
"Stored YOLO chunk {} (frame {}) for video {}",
|
||||||
|
i,
|
||||||
|
frame.frame,
|
||||||
|
uuid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to store YOLO chunk {}: {}", i, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn store_ocr_chunks(
|
||||||
|
db: &PostgresDb,
|
||||||
|
uuid: &str,
|
||||||
|
ocr_result: &OcrResult,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Get video record to obtain file_id and fps
|
||||||
|
let video = match db.get_video_by_uuid(uuid).await {
|
||||||
|
Ok(Some(video)) => video,
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::error!("Video not found for uuid: {}", uuid);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to get video for uuid {}: {}", uuid, e);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let file_id = video.id;
|
||||||
|
let fps = if video.fps > 0.0 { video.fps } else { 30.0 };
|
||||||
|
|
||||||
|
for (i, frame) in ocr_result.frames.iter().enumerate() {
|
||||||
|
let mut chunk = Chunk::new(
|
||||||
|
file_id as i32,
|
||||||
|
uuid.to_string(),
|
||||||
|
i as u32,
|
||||||
|
ChunkType::Trace,
|
||||||
|
ChunkRule::Rule1,
|
||||||
|
frame.frame as i64,
|
||||||
|
frame.frame as i64 + 1,
|
||||||
|
fps,
|
||||||
|
serde_json::json!({
|
||||||
|
"texts": frame.texts,
|
||||||
|
"timestamp": frame.timestamp,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Override chunk_id to include processor prefix for uniqueness
|
||||||
|
chunk.chunk_id = format!("trace_ocr_{:04}", i);
|
||||||
|
|
||||||
|
match db.store_chunk(&chunk).await {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::info!(
|
||||||
|
"Stored OCR chunk {} (frame {}) for video {}",
|
||||||
|
i,
|
||||||
|
frame.frame,
|
||||||
|
uuid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to store OCR chunk {}: {}", i, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn store_face_chunks(
|
||||||
|
db: &PostgresDb,
|
||||||
|
uuid: &str,
|
||||||
|
face_result: &FaceResult,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Get video record to obtain file_id and fps
|
||||||
|
let video = match db.get_video_by_uuid(uuid).await {
|
||||||
|
Ok(Some(video)) => video,
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::error!("Video not found for uuid: {}", uuid);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to get video for uuid {}: {}", uuid, e);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let file_id = video.id;
|
||||||
|
let fps = if video.fps > 0.0 { video.fps } else { 30.0 };
|
||||||
|
|
||||||
|
for (i, frame) in face_result.frames.iter().enumerate() {
|
||||||
|
let mut chunk = Chunk::new(
|
||||||
|
file_id as i32,
|
||||||
|
uuid.to_string(),
|
||||||
|
i as u32,
|
||||||
|
ChunkType::Trace,
|
||||||
|
ChunkRule::Rule1,
|
||||||
|
frame.frame as i64,
|
||||||
|
frame.frame as i64 + 1,
|
||||||
|
fps,
|
||||||
|
serde_json::json!({
|
||||||
|
"faces": frame.faces,
|
||||||
|
"timestamp": frame.timestamp,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Override chunk_id to include processor prefix for uniqueness
|
||||||
|
chunk.chunk_id = format!("trace_face_{:04}", i);
|
||||||
|
|
||||||
|
match db.store_chunk(&chunk).await {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::info!(
|
||||||
|
"Stored FACE chunk {} (frame {}) for video {}",
|
||||||
|
i,
|
||||||
|
frame.frame,
|
||||||
|
uuid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to store FACE chunk {}: {}", i, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn store_pose_chunks(
|
||||||
|
db: &PostgresDb,
|
||||||
|
uuid: &str,
|
||||||
|
pose_result: &PoseResult,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Get video record to obtain file_id and fps
|
||||||
|
let video = match db.get_video_by_uuid(uuid).await {
|
||||||
|
Ok(Some(video)) => video,
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::error!("Video not found for uuid: {}", uuid);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to get video for uuid {}: {}", uuid, e);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let file_id = video.id;
|
||||||
|
let fps = if video.fps > 0.0 { video.fps } else { 30.0 };
|
||||||
|
|
||||||
|
for (i, frame) in pose_result.frames.iter().enumerate() {
|
||||||
|
let mut chunk = Chunk::new(
|
||||||
|
file_id as i32,
|
||||||
|
uuid.to_string(),
|
||||||
|
i as u32,
|
||||||
|
ChunkType::Trace,
|
||||||
|
ChunkRule::Rule1,
|
||||||
|
frame.frame as i64,
|
||||||
|
frame.frame as i64 + 1,
|
||||||
|
fps,
|
||||||
|
serde_json::json!({
|
||||||
|
"persons": frame.persons,
|
||||||
|
"timestamp": frame.timestamp,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Override chunk_id to include processor prefix for uniqueness
|
||||||
|
chunk.chunk_id = format!("trace_pose_{:04}", i);
|
||||||
|
|
||||||
|
match db.store_chunk(&chunk).await {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::info!(
|
||||||
|
"Stored POSE chunk {} (frame {}) for video {}",
|
||||||
|
i,
|
||||||
|
frame.frame,
|
||||||
|
uuid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to store POSE chunk {}: {}", i, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn store_asrx_chunks(
|
||||||
|
db: &PostgresDb,
|
||||||
|
uuid: &str,
|
||||||
|
asrx_result: &AsrxResult,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Get video record to obtain file_id and fps
|
||||||
|
let video = match db.get_video_by_uuid(uuid).await {
|
||||||
|
Ok(Some(video)) => video,
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::error!("Video not found for uuid: {}", uuid);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to get video for uuid {}: {}", uuid, e);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let file_id = video.id;
|
||||||
|
let fps = if video.fps > 0.0 { video.fps } else { 30.0 };
|
||||||
|
|
||||||
|
for (i, segment) in asrx_result.segments.iter().enumerate() {
|
||||||
|
let mut chunk = Chunk::from_seconds(
|
||||||
|
file_id as i32,
|
||||||
|
uuid.to_string(),
|
||||||
|
i as u32,
|
||||||
|
ChunkType::Trace,
|
||||||
|
ChunkRule::Rule1,
|
||||||
|
segment.start,
|
||||||
|
segment.end,
|
||||||
|
fps,
|
||||||
|
serde_json::json!({
|
||||||
|
"text": segment.text,
|
||||||
|
"timestamp": segment.start,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Override chunk_id to include processor prefix for uniqueness
|
||||||
|
chunk.chunk_id = format!("trace_asrx_{:04}", i);
|
||||||
|
|
||||||
|
match db.store_chunk(&chunk).await {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::info!("Stored ASRX chunk {} for video {}", i, uuid);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to store ASRX chunk {}: {}", i, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_running_count(&self) -> usize {
|
pub async fn get_running_count(&self) -> usize {
|
||||||
*self.running_count.read().await
|
*self.running_count.read().await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def update_file(filename, new_js_code):
|
||||||
|
with open(filename, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
for node in data["nodes"]:
|
||||||
|
if "parameters" in node and "jsCode" in node["parameters"]:
|
||||||
|
print(f"Updating jsCode in node: {node.get('name', 'unknown')}")
|
||||||
|
node["parameters"]["jsCode"] = new_js_code
|
||||||
|
break
|
||||||
|
|
||||||
|
with open(filename, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
print(f"Updated {filename}")
|
||||||
|
|
||||||
|
|
||||||
|
# New jsCode for video search (already updated)
|
||||||
|
js_code_video_search = """const hits = $input.first().json.hits;
|
||||||
|
|
||||||
|
if (!hits || hits.length === 0) {
|
||||||
|
return {
|
||||||
|
json: {
|
||||||
|
message: '找不到相關結果',
|
||||||
|
query: $input.first().json.query
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = hits.map((hit, index) => {
|
||||||
|
return {
|
||||||
|
number: index + 1,
|
||||||
|
text: hit.text,
|
||||||
|
start: hit.start,
|
||||||
|
end: hit.end,
|
||||||
|
score: Math.round(hit.score * 100) + '%',
|
||||||
|
video_title: hit.title,
|
||||||
|
file_path: hit.file_path
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: {
|
||||||
|
query: $input.first().json.query,
|
||||||
|
count: $input.first().json.count,
|
||||||
|
results: results
|
||||||
|
}
|
||||||
|
};"""
|
||||||
|
|
||||||
|
# New jsCode for simple workflow
|
||||||
|
js_code_simple = """// 處理 Momentry 搜尋結果
|
||||||
|
const data = $input.first().json;
|
||||||
|
const hits = data.hits;
|
||||||
|
|
||||||
|
if (!hits || hits.length === 0) {
|
||||||
|
return {
|
||||||
|
json: {
|
||||||
|
success: false,
|
||||||
|
message: '找不到相關結果',
|
||||||
|
query: data.query
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化結果
|
||||||
|
const formattedResults = hits.map((hit, idx) => {
|
||||||
|
return {
|
||||||
|
index: idx + 1,
|
||||||
|
id: hit.id,
|
||||||
|
title: hit.title,
|
||||||
|
text: hit.text,
|
||||||
|
startTime: hit.start,
|
||||||
|
endTime: hit.end,
|
||||||
|
relevance: Math.round(hit.score * 100) + '%',
|
||||||
|
file_path: hit.file_path
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: {
|
||||||
|
success: true,
|
||||||
|
query: data.query,
|
||||||
|
totalFound: data.count,
|
||||||
|
results: formattedResults
|
||||||
|
}
|
||||||
|
};"""
|
||||||
|
|
||||||
|
# New jsCode for RAG MCP workflow
|
||||||
|
js_code_rag = """// Process Momentry Core search results
|
||||||
|
const data = $input.first().json;
|
||||||
|
const hits = data.hits || [];
|
||||||
|
|
||||||
|
if (hits.length === 0) {
|
||||||
|
return {
|
||||||
|
json: {
|
||||||
|
success: false,
|
||||||
|
message: 'No relevant results found',
|
||||||
|
query: data.query,
|
||||||
|
results: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format results for RAG
|
||||||
|
const formattedResults = hits.map((hit, idx) => {
|
||||||
|
return {
|
||||||
|
index: idx + 1,
|
||||||
|
id: hit.id || hit.chunk_id,
|
||||||
|
title: hit.title || 'Unknown Video',
|
||||||
|
text: hit.text || hit.content || '',
|
||||||
|
startTime: hit.start_time || hit.start || 0,
|
||||||
|
endTime: hit.end_time || hit.end || 0,
|
||||||
|
relevance: Math.round((hit.score || 0) * 100) + '%',
|
||||||
|
videoUuid: hit.video_uuid || hit.uuid,
|
||||||
|
file_path: hit.file_path || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build context for RAG
|
||||||
|
const context = formattedResults
|
||||||
|
.map(r => \`[\${r.index}] \${r.text} (Video: \${r.title}, Time: \${r.startTime}s-\${r.endTime}s)\`)
|
||||||
|
.join('\\n\\n');
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: {
|
||||||
|
success: true,
|
||||||
|
query: data.query,
|
||||||
|
totalFound: data.count || hits.length,
|
||||||
|
context: context,
|
||||||
|
results: formattedResults
|
||||||
|
}
|
||||||
|
};"""
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Update simple workflow
|
||||||
|
update_file("docs/n8n_workflow_simple.json", js_code_simple)
|
||||||
|
# Update RAG workflow
|
||||||
|
update_file("docs/n8n_workflow_video_rag_mcp.json", js_code_rag)
|
||||||
|
# Note: video search already updated, but we can re-update if needed
|
||||||
|
# update_file('docs/n8n_workflow_video_search.json', js_code_video_search)
|
||||||
|
print("All workflows updated.")
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Updating Momentry API service with DB connection pool settings..."
|
||||||
|
|
||||||
|
# Backup existing plist
|
||||||
|
sudo cp /Library/LaunchDaemons/com.momentry.api.plist /Library/LaunchDaemons/com.momentry.api.plist.backup.$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
# Stop the service
|
||||||
|
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist 2>/dev/null || true
|
||||||
|
|
||||||
|
# Copy updated plist
|
||||||
|
sudo cp /Users/accusys/momentry_core_0.1/com.momentry.api.updated.plist /Library/LaunchDaemons/com.momentry.api.plist
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
||||||
|
|
||||||
|
echo "API service updated successfully."
|
||||||
|
echo "Checking service status..."
|
||||||
|
launchctl list | grep com.momentry.api || echo "Service not listed in user domain; check system domain."
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Find latest ASR file for job 10
|
||||||
|
output_dir = "/Users/accusys/momentry/output"
|
||||||
|
files = [f for f in os.listdir(output_dir) if f.startswith("job_10_asr_")]
|
||||||
|
if not files:
|
||||||
|
print("No ASR files found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sort by timestamp (numeric suffix)
|
||||||
|
def extract_timestamp(fname):
|
||||||
|
# job_10_asr_1774505428450.json
|
||||||
|
parts = fname.split("_")
|
||||||
|
timestamp = parts[3].split(".")[0]
|
||||||
|
return int(timestamp)
|
||||||
|
|
||||||
|
files.sort(key=extract_timestamp, reverse=True)
|
||||||
|
latest_file = os.path.join(output_dir, files[0])
|
||||||
|
print(f"Using ASR file: {latest_file}")
|
||||||
|
|
||||||
|
with open(latest_file, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# Convert to JSON string, escape single quotes for SQL
|
||||||
|
json_str = json.dumps(data).replace("'", "''")
|
||||||
|
|
||||||
|
# Update processor_results
|
||||||
|
sql = f"""
|
||||||
|
UPDATE processor_results
|
||||||
|
SET status = 'completed',
|
||||||
|
output_data = '{json_str}'::jsonb,
|
||||||
|
completed_at = NOW()
|
||||||
|
WHERE job_id = 10 AND processor = 'asr';
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Execute with psql
|
||||||
|
db_url = "postgres://accusys@localhost:5432/momentry"
|
||||||
|
cmd = ["psql", db_url, "-c", sql]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Error updating database: {result.stderr}")
|
||||||
|
else:
|
||||||
|
print("Successfully updated ASR processor result to completed")
|
||||||
|
|
||||||
|
# Also need to store ASR chunks in database via Rust logic
|
||||||
|
# For now, we'll trust that the worker will do it when restarted
|
||||||
|
# (the store_asr_chunks method will be called on completion)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def update_workflow(filename):
|
||||||
|
with open(filename, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# Find the code node (index 2)
|
||||||
|
for node in data["nodes"]:
|
||||||
|
if (
|
||||||
|
node.get("name") == "處理結果"
|
||||||
|
or "parameters" in node
|
||||||
|
and "jsCode" in node["parameters"]
|
||||||
|
):
|
||||||
|
old_code = node["parameters"]["jsCode"]
|
||||||
|
print("Old code length:", len(old_code))
|
||||||
|
# New code without URL generation
|
||||||
|
new_code = """const hits = $input.first().json.hits;
|
||||||
|
|
||||||
|
if (!hits || hits.length === 0) {
|
||||||
|
return {
|
||||||
|
json: {
|
||||||
|
message: '找不到相關結果',
|
||||||
|
query: $input.first().json.query
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = hits.map((hit, index) => {
|
||||||
|
return {
|
||||||
|
number: index + 1,
|
||||||
|
text: hit.text,
|
||||||
|
start: hit.start,
|
||||||
|
end: hit.end,
|
||||||
|
score: Math.round(hit.score * 100) + '%',
|
||||||
|
video_title: hit.title,
|
||||||
|
file_path: hit.file_path
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: {
|
||||||
|
query: $input.first().json.query,
|
||||||
|
count: $input.first().json.count,
|
||||||
|
results: results
|
||||||
|
}
|
||||||
|
};"""
|
||||||
|
node["parameters"]["jsCode"] = new_code
|
||||||
|
print("Updated jsCode")
|
||||||
|
break
|
||||||
|
|
||||||
|
with open(filename, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
print("File updated:", filename)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
update_workflow(sys.argv[1])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
65327
|
||||||
Reference in New Issue
Block a user