diff --git a/ktor/src/main/kotlin/ink/snowflake/server/Application.kt b/ktor/src/main/kotlin/ink/snowflake/server/Application.kt index 20668cb..a6cf998 100644 --- a/ktor/src/main/kotlin/ink/snowflake/server/Application.kt +++ b/ktor/src/main/kotlin/ink/snowflake/server/Application.kt @@ -15,7 +15,8 @@ import io.ktor.server.application.* import io.ktor.server.tomcat.jakarta.* -const val VIDEO_INPUT_PATH = "/tmp/" +//const val VIDEO_INPUT_PATH = "/tmp/" +const val VIDEO_INPUT_PATH = "C:/tmp/" /** * 服务器地址 diff --git a/ktor/src/main/kotlin/ink/snowflake/server/database/ImageDao.kt b/ktor/src/main/kotlin/ink/snowflake/server/database/ImageDao.kt index cd8dcd8..761276f 100644 --- a/ktor/src/main/kotlin/ink/snowflake/server/database/ImageDao.kt +++ b/ktor/src/main/kotlin/ink/snowflake/server/database/ImageDao.kt @@ -16,7 +16,6 @@ object ImageDao { fun insertImageAnalyticsData(request: ImageAnalyticsRequest) { return transaction { ImageTable.insert { - it[object_name] = request.object_name it[upload_datetime] = Timestamp.valueOf(request.upload_datetime) .toLocalDateTime().toKotlinLocalDateTime() it[file_name] = request.file_name @@ -34,15 +33,19 @@ object ImageDao { } - fun getVideoList(): List { + fun getImageList(name: String): List { return transaction { ImageTable.selectAll() + .where { ImageTable.name like "%$name%" } .orderBy(ImageTable.upload_datetime, SortOrder.DESC) .map { ImageAnalyticsRequest( - object_name = "http://${SERVER_PATH_OSS}:9000/image/" + it[ImageTable.object_name], + id = it[ImageTable.id].value, + name = it[ImageTable.name], upload_datetime = formatLocalDateTimeToString(it[ImageTable.upload_datetime]), file_name = it[ImageTable.file_name], + image_pre = "http://${SERVER_PATH_OSS}:9000/image-sca/" + it[ImageTable.image_pre], + image_after = "http://${SERVER_PATH_OSS}:9000/image-sca/" + it[ImageTable.image_after], resolution = it[ImageTable.resolution], size = it[ImageTable.size], cocoon_count = it[ImageTable.cocoon_count], diff --git a/ktor/src/main/kotlin/ink/snowflake/server/database/VideoDao.kt b/ktor/src/main/kotlin/ink/snowflake/server/database/VideoDao.kt index 3b3d838..1c43eda 100644 --- a/ktor/src/main/kotlin/ink/snowflake/server/database/VideoDao.kt +++ b/ktor/src/main/kotlin/ink/snowflake/server/database/VideoDao.kt @@ -72,7 +72,6 @@ object VideoDao { } } - fun getAnalyticsDetailByVideoId(vId: Int): ResultRow? { return transaction { VideoTable.selectAll().where { VideoTable.id eq vId }.singleOrNull() diff --git a/ktor/src/main/kotlin/ink/snowflake/server/database/table/ImageTable.kt b/ktor/src/main/kotlin/ink/snowflake/server/database/table/ImageTable.kt index 0a68c58..422490c 100644 --- a/ktor/src/main/kotlin/ink/snowflake/server/database/table/ImageTable.kt +++ b/ktor/src/main/kotlin/ink/snowflake/server/database/table/ImageTable.kt @@ -6,10 +6,12 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable import org.jetbrains.exposed.v1.datetime.datetime import org.jetbrains.exposed.v1.json.json -object ImageTable : IntIdTable("image") { - val object_name = varchar("object_name", 255) +object ImageTable : IntIdTable("image_sca") { + val name = varchar("name", 255) val upload_datetime = datetime("upload_datetime") val file_name = varchar("file_name", 255) + val image_pre = varchar("image_pre", 255) + val image_after = varchar("image_after", 255) val resolution = varchar("resolution", 255) val size = float("size") val cocoon_count = float("cocoon_count") diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/request/ImageAnalyticsRequest.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/request/ImageAnalyticsRequest.kt index 8675791..776a2c6 100644 --- a/ktor/src/main/kotlin/ink/snowflake/server/model/request/ImageAnalyticsRequest.kt +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/request/ImageAnalyticsRequest.kt @@ -4,10 +4,13 @@ import kotlinx.serialization.Serializable @Serializable data class ImageAnalyticsRequest( - val object_name: String, // Minio存储名 + val id : Int, + val name : String, val upload_datetime: String, // 上传时间 val file_name: String, // 文件名 val resolution: String, // 图片分辨率 + val image_after: String, + val image_pre: String, val size: Float, // 文件大小,单位MB val cocoon_count: Float, // 识别出的茧数量 val max_confidence: Float, // 最大置信度 diff --git a/ktor/src/main/kotlin/ink/snowflake/server/route/ImageAnalytics.kt b/ktor/src/main/kotlin/ink/snowflake/server/route/ImageAnalytics.kt index 1c7f110..f8238a8 100644 --- a/ktor/src/main/kotlin/ink/snowflake/server/route/ImageAnalytics.kt +++ b/ktor/src/main/kotlin/ink/snowflake/server/route/ImageAnalytics.kt @@ -9,9 +9,9 @@ import io.ktor.server.routing.* import io.ktor.server.request.* import io.ktor.server.response.* -fun Application.ImageAnalytics() { +fun Application.ImageAnalytics() { routing { - route("/api") { + route("/api/sca") { // 上传分析结果 post("/saveImageAnalyticsData") { val request = call.receive() @@ -28,9 +28,10 @@ fun Application.ImageAnalytics() { stopHLSStream(camera) call.respond(BaseResponse(message = "摄像头流已停止", data = null)) } - // 获取已分析视频列表 + // 获取已分析图片列表 get("/getImageList") { - val res = ImageDao.getVideoList() + val name = call.parameters["name"] + val res = ImageDao.getImageList(name ?: "") call.respond(BaseResponse(data = res)) } } diff --git a/ktor/src/main/kotlin/ink/snowflake/server/route/VideoAnalytics.kt b/ktor/src/main/kotlin/ink/snowflake/server/route/VideoAnalytics.kt index 731cdb5..d03a8a6 100644 --- a/ktor/src/main/kotlin/ink/snowflake/server/route/VideoAnalytics.kt +++ b/ktor/src/main/kotlin/ink/snowflake/server/route/VideoAnalytics.kt @@ -2,6 +2,7 @@ package ink.snowflake.server.route import ink.snowflake.server.SERVER_PATH_OSS import ink.snowflake.server.VIDEO_INPUT_PATH +import ink.snowflake.server.database.VideoDao import ink.snowflake.server.database.table.VideoTable import ink.snowflake.server.database.table.VideoTable.vAAverageMaskedRatio import ink.snowflake.server.database.table.VideoTable.vACountPeople @@ -12,17 +13,19 @@ import ink.snowflake.server.database.table.VideoTable.vStartDateTime import ink.snowflake.server.model.request.VideoAnalyticsRequest import ink.snowflake.server.model.response.* import ink.snowflake.server.utils.WebSocketManager.broadcastMessage -import ink.snowflake.server.database.VideoDao import ink.snowflake.server.utils.runCommand import io.ktor.http.content.* import io.ktor.server.application.* import io.ktor.server.auth.* -import io.ktor.server.routing.* import io.ktor.server.request.* import io.ktor.server.response.* +import io.ktor.server.routing.* import io.ktor.server.websocket.* import io.ktor.websocket.* +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.datetime.LocalDateTime import kotlinx.datetime.toJavaLocalDateTime import java.io.File @@ -67,8 +70,7 @@ fun Application.VideoAnalytics() { clients.remove(this) // 确保在连接关闭时移除客户端 } } - route("/api") { - route("/iva") { + route("/api/iva") { // 上传分析结果 post("/saveVideoAnalyticsData") { val request = call.receive() @@ -76,7 +78,7 @@ fun Application.VideoAnalytics() { call.respond(BaseResponse(data = VideoDao.insertVideoAnalyticsData(request))) } authenticate { - post("/upload") { + post("/createVideoTask") { val multipart = call.receiveMultipart() //1G // 确保 uploads 目录存在 val uploadDir = File(VIDEO_INPUT_PATH) @@ -90,32 +92,35 @@ fun Application.VideoAnalytics() { var name = "" var datetime = "" broadcastMessage("正在上传数据") - multipart.forEachPart { part -> - when (part) { - is PartData.FileItem -> { - fileName = part.originalFileName ?: "unknown" - val file = File("$VIDEO_INPUT_PATH$fileName") // 保存路径 - //ktor3 + + withContext(Dispatchers.IO) { + multipart.forEachPart { part -> + when (part) { + is PartData.FileItem -> { + fileName = part.originalFileName ?: "unknown" + val file = File("$VIDEO_INPUT_PATH$fileName") // 保存路径 + //ktor3 // file.outputStream().use { outputStream -> // val writableChannel = Channels.newChannel(outputStream) // part.provider().copyTo(writableChannel) // 复制到 WritableByteChannel // } - //ktor2 - part.streamProvider().use { inputStream -> - file.outputStream().buffered().use { outputStream -> - inputStream.copyTo(outputStream) + //ktor2 + part.streamProvider().use { inputStream -> + file.outputStream().buffered().use { outputStream -> + inputStream.copyTo(outputStream) + } } } - } - is PartData.FormItem -> { - when (part.name) { - "name" -> name = part.value - "datetime" -> datetime = part.value + is PartData.FormItem -> { + when (part.name) { + "projectName" -> name = part.value + "projectDatetime" -> datetime = part.value + } } - } - else -> part.dispose() + else -> part.dispose() + } } } call.respond(BaseResponse(message = "上传成功", data = null)) @@ -267,7 +272,6 @@ fun Application.VideoAnalytics() { ) } } - } } } } diff --git a/vue/.vscode/launch.json b/vue/.vscode/launch.json index 1302742..d3bcacb 100644 --- a/vue/.vscode/launch.json +++ b/vue/.vscode/launch.json @@ -13,7 +13,7 @@ }, { "type": "chrome", - "name": "智能控制中心", + "name": "主干AI实验室", "request": "launch", "url": "http://localhost:5666", "env": { "NODE_ENV": "development" }, diff --git a/vue/apps/web-antd/.env b/vue/apps/web-antd/.env index 184576f..978688a 100644 --- a/vue/apps/web-antd/.env +++ b/vue/apps/web-antd/.env @@ -1,5 +1,5 @@ # 应用标题 -VITE_APP_TITLE=智能控制中心 +VITE_APP_TITLE=主干AI实验室 # 应用命名空间,用于缓存、store等功能的前缀,确保隔离 VITE_APP_NAMESPACE=vben-web-antd diff --git a/vue/apps/web-antd/src/api/core/index.ts b/vue/apps/web-antd/src/api/core/index.ts index ea1479d..ff6ce62 100644 --- a/vue/apps/web-antd/src/api/core/index.ts +++ b/vue/apps/web-antd/src/api/core/index.ts @@ -2,4 +2,5 @@ export * from './auth'; export * from './iva'; export * from './menu'; export * from './remote'; +export * from './sca'; export * from './user'; diff --git a/vue/apps/web-antd/src/api/core/iva.ts b/vue/apps/web-antd/src/api/core/iva.ts index 81e6807..828890d 100644 --- a/vue/apps/web-antd/src/api/core/iva.ts +++ b/vue/apps/web-antd/src/api/core/iva.ts @@ -8,10 +8,21 @@ export async function refreshVideoList(name = '') { } /** - * 获取已分析的视频列表 + * 获取已分析的视频 */ export async function refreshVideoDetail(vId = '') { return requestClient.get('/iva/getAnalyticsDetailByVideoId', { params: { vId }, }); } + +/** + * 上传视频分析任务 + */ +export async function createVideoTask(formData: FormData) { + return requestClient.post('/iva/createVideoTask', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); +} diff --git a/vue/apps/web-antd/src/api/core/sca.ts b/vue/apps/web-antd/src/api/core/sca.ts new file mode 100644 index 0000000..f68531a --- /dev/null +++ b/vue/apps/web-antd/src/api/core/sca.ts @@ -0,0 +1,19 @@ +import { requestClient } from '#/api/request'; + +/** + * 获取已分析的图片列表 + */ +export async function refreshImageList(name = '') { + return requestClient.get('/sca/getImageList', { params: { name } }); +} + +/** + * 上传图片分析任务 + */ +export async function createImageTask(formData: FormData) { + return requestClient.post('/sca/createImageTask', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); +} diff --git a/vue/apps/web-antd/src/locales/langs/zh-CN/ai.json b/vue/apps/web-antd/src/locales/langs/zh-CN/ai.json index 54241ab..8c0e50b 100644 --- a/vue/apps/web-antd/src/locales/langs/zh-CN/ai.json +++ b/vue/apps/web-antd/src/locales/langs/zh-CN/ai.json @@ -1,5 +1,6 @@ { "title": "人工智能", "intelligence_video_analysis": "视频智能分析", + "silkworm_cocoon_analysis": "蚕茧仪评分析", "young_silkworm_analysis": "催青阶段分析" } diff --git a/vue/apps/web-antd/src/router/routes/modules/ai.ts b/vue/apps/web-antd/src/router/routes/modules/ai.ts index 9e90569..f232735 100644 --- a/vue/apps/web-antd/src/router/routes/modules/ai.ts +++ b/vue/apps/web-antd/src/router/routes/modules/ai.ts @@ -8,7 +8,7 @@ const routes: RouteRecordRaw[] = [ { meta: { icon: 'ic:baseline-view-in-ar', - keepAlive: true, + keepAlive: false, order: 2, title: $t('ai.title'), }, @@ -22,9 +22,20 @@ const routes: RouteRecordRaw[] = [ authority: ['iva'], icon: 'mdi:video', title: $t('ai.intelligence_video_analysis'), + keepAlive: false, }, component: () => import('#/views/ai/iva/index.vue'), }, + { + name: 'SCA', + path: '/ai/sca', + meta: { + authority: ['sca'], + icon: 'mdi:ice-cream', + title: $t('ai.silkworm_cocoon_analysis'), + }, + component: () => import('#/views/ai/sca/index.vue'), + }, { name: 'YSA', path: '/ai/ysa', diff --git a/vue/apps/web-antd/src/views/ai/iva/CreateVideoTaskModal.vue b/vue/apps/web-antd/src/views/ai/iva/CreateVideoTaskModal.vue new file mode 100644 index 0000000..9e312c0 --- /dev/null +++ b/vue/apps/web-antd/src/views/ai/iva/CreateVideoTaskModal.vue @@ -0,0 +1,159 @@ + + + diff --git a/vue/apps/web-antd/src/views/ai/iva/index.vue b/vue/apps/web-antd/src/views/ai/iva/index.vue index b386ffc..bb40686 100644 --- a/vue/apps/web-antd/src/views/ai/iva/index.vue +++ b/vue/apps/web-antd/src/views/ai/iva/index.vue @@ -6,7 +6,7 @@ import type { EchartsUIType } from '@vben/plugins/echarts'; import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; -import { AnalysisOverview } from '@vben/common-ui'; +import { AnalysisOverview, useVbenModal } from '@vben/common-ui'; import { SvgBellIcon, SvgCakeIcon, @@ -20,6 +20,8 @@ import videojs from 'video.js'; import * as api from '#/api'; +import CreateVideoTaskModal from './CreateVideoTaskModal.vue'; + import 'video.js/dist/video-js.css'; const list = ref([]); @@ -40,8 +42,14 @@ function refreshList() { loadList(); message.success('视频列表加载完成'); } +const [BaseModal, baseModalApi] = useVbenModal({ + // 连接抽离的组件 + connectedComponent: CreateVideoTaskModal, +}); -const createTask = () => {}; +function createTask() { + baseModalApi.open(); +} async function selectItem(item: any) { const res = await api.refreshVideoDetail(item.v_id); @@ -314,7 +322,6 @@ function refreshLineChart() { refreshVideoPlayer(); } } - function onListItemClick(video: any) { // 视频跳转到指定时间点 const vStartTime = @@ -338,6 +345,8 @@ function onListItemClick(video: any) { - +