v1.0.2发布,支持视频分析结果查看
This commit is contained in:
@@ -2,13 +2,13 @@ package ink.snowflake.server
|
||||
|
||||
import com.google.gson.Gson
|
||||
import ink.snowflake.server.plugins.*
|
||||
import ink.snowflake.server.route.func.User
|
||||
import ink.snowflake.server.route.func.chat
|
||||
import ink.snowflake.server.route.configureSockets
|
||||
import ink.snowflake.server.route.func.ImageAnalytics
|
||||
import ink.snowflake.server.route.func.RemoteDebug
|
||||
import ink.snowflake.server.route.func.VideoAnalytics
|
||||
import ink.snowflake.server.route.func.VideoAnalyticsJetson
|
||||
import ink.snowflake.server.route.User
|
||||
import ink.snowflake.server.route.chat
|
||||
import ink.snowflake.server.plugins.configureSockets
|
||||
import ink.snowflake.server.route.ImageAnalytics
|
||||
import ink.snowflake.server.route.RemoteDebug
|
||||
import ink.snowflake.server.route.VideoAnalytics
|
||||
import ink.snowflake.server.route.VideoAnalyticsJetson
|
||||
import ink.snowflake.server.route.mainFunc
|
||||
import ink.snowflake.server.utils.AppConfig
|
||||
import io.ktor.server.application.*
|
||||
@@ -19,9 +19,14 @@ const val VIDEO_INPUT_PATH = "/tmp/"
|
||||
|
||||
/**
|
||||
* 服务器地址
|
||||
* ADB 秦朗FRP地址
|
||||
*/
|
||||
const val SERVER_PATH = "171.212.101.201"
|
||||
//const val SERVER_PATH = "localhost"
|
||||
const val SERVER_PATH_FRP = "171.212.101.201"
|
||||
/**
|
||||
* 服务器地址
|
||||
* OSS 对象存储服务器地址
|
||||
*/
|
||||
const val SERVER_PATH_OSS = "171.212.101.199"
|
||||
|
||||
val gson = Gson()
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package ink.snowflake.server.database
|
||||
|
||||
import ink.snowflake.server.database.table.AIProfile
|
||||
import ink.snowflake.server.database.table.AIProfilesTable
|
||||
import ink.snowflake.server.database.table.toAIProfile
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
|
||||
object AIDao {
|
||||
/**
|
||||
* 获取所有AI
|
||||
*/
|
||||
fun getAllAIProfiles(): List<AIProfile> {
|
||||
return transaction {
|
||||
AIProfilesTable.selectAll().map { it.toAIProfile() }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+9
-7
@@ -1,12 +1,14 @@
|
||||
package ink.snowflake.server.utils.database
|
||||
package ink.snowflake.server.database
|
||||
|
||||
import ink.snowflake.server.model.database.*
|
||||
import ink.snowflake.server.database.table.ChatRecord
|
||||
import ink.snowflake.server.database.table.ChatRecordsTable
|
||||
import ink.snowflake.server.database.table.toChatRecord
|
||||
import kotlinx.datetime.Clock
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestampLiteral
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.datetime.timestampLiteral
|
||||
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
|
||||
object ChatRecordsDao {
|
||||
/**
|
||||
+9
-10
@@ -1,15 +1,14 @@
|
||||
package ink.snowflake.server.utils.database
|
||||
package ink.snowflake.server.database
|
||||
|
||||
import ink.snowflake.server.SERVER_PATH
|
||||
import ink.snowflake.server.model.database.ImageAnalyticsRequest
|
||||
import ink.snowflake.server.model.database.ImageTable
|
||||
import ink.snowflake.server.model.response.VideoListResponse
|
||||
import ink.snowflake.server.SERVER_PATH_OSS
|
||||
import ink.snowflake.server.model.request.ImageAnalyticsRequest
|
||||
import ink.snowflake.server.database.table.ImageTable
|
||||
import ink.snowflake.server.utils.formatLocalDateTimeToString
|
||||
import kotlinx.datetime.toKotlinLocalDateTime
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import java.sql.Timestamp
|
||||
|
||||
object ImageDao {
|
||||
@@ -41,7 +40,7 @@ object ImageDao {
|
||||
.orderBy(ImageTable.upload_datetime, SortOrder.DESC)
|
||||
.map {
|
||||
ImageAnalyticsRequest(
|
||||
object_name = "http://${SERVER_PATH}:9000/image/" + it[ImageTable.object_name],
|
||||
object_name = "http://${SERVER_PATH_OSS}:9000/image/" + it[ImageTable.object_name],
|
||||
upload_datetime = formatLocalDateTimeToString(it[ImageTable.upload_datetime]),
|
||||
file_name = it[ImageTable.file_name],
|
||||
resolution = it[ImageTable.resolution],
|
||||
+10
-13
@@ -1,18 +1,14 @@
|
||||
package ink.snowflake.server.utils.database
|
||||
package ink.snowflake.server.database
|
||||
|
||||
import ink.snowflake.server.model.database.ImageTable
|
||||
import ink.snowflake.server.model.database.UserTable
|
||||
import ink.snowflake.server.database.table.UserTable
|
||||
import ink.snowflake.server.model.response.UserInfo
|
||||
import ink.snowflake.server.utils.formatLocalDateTimeToString
|
||||
import kotlinx.datetime.Clock
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestampLiteral
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.v1.datetime.timestampLiteral
|
||||
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
|
||||
|
||||
object UserDAO {
|
||||
/**
|
||||
@@ -82,7 +78,8 @@ object UserDAO {
|
||||
UserInfo(
|
||||
username = it[UserTable.username],
|
||||
email = it[UserTable.email],
|
||||
phone = it[UserTable.phone]
|
||||
phone = it[UserTable.phone],
|
||||
roles = it[UserTable.roles]?.removePrefix("{")!!.removeSuffix("}").split(","),
|
||||
)
|
||||
}
|
||||
.singleOrNull()
|
||||
+22
-17
@@ -1,20 +1,20 @@
|
||||
package ink.snowflake.server.utils.database
|
||||
package ink.snowflake.server.database
|
||||
|
||||
import ink.snowflake.server.model.database.VideoAnalyticsDetailTable
|
||||
import ink.snowflake.server.model.database.VideoTable
|
||||
import ink.snowflake.server.model.database.VideoTable.vATime
|
||||
import ink.snowflake.server.model.database.VideoTable.vName
|
||||
import ink.snowflake.server.database.table.VideoAnalyticsDetailTable
|
||||
import ink.snowflake.server.database.table.VideoTable
|
||||
import ink.snowflake.server.database.table.VideoTable.vATime
|
||||
import ink.snowflake.server.database.table.VideoTable.vName
|
||||
import ink.snowflake.server.model.request.VideoAnalyticsRequest
|
||||
import ink.snowflake.server.model.request.VideoDetail
|
||||
import ink.snowflake.server.model.response.VideoListResponse
|
||||
import ink.snowflake.server.utils.formatLocalDateTimeToString
|
||||
import kotlinx.datetime.toKotlinLocalDateTime
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import java.sql.Timestamp
|
||||
|
||||
object VideoDao {
|
||||
@@ -54,27 +54,32 @@ object VideoDao {
|
||||
}
|
||||
}
|
||||
|
||||
fun getVideoList():List<VideoListResponse> {
|
||||
fun getVideoList(name: String?): List<VideoListResponse> {
|
||||
return transaction {
|
||||
VideoTable.selectAll()
|
||||
.orderBy(vATime, SortOrder.DESC)
|
||||
val query = VideoTable.selectAll()
|
||||
// 动态加 where 条件
|
||||
if (!name.isNullOrBlank()) {
|
||||
query.where { VideoTable.vName like "%${name}%" }
|
||||
}
|
||||
query.orderBy(VideoTable.vATime, SortOrder.DESC)
|
||||
.map {
|
||||
VideoListResponse(
|
||||
v_id = it[VideoTable.id].value,
|
||||
v_name = it[vName],
|
||||
v_a_time = formatLocalDateTimeToString(it[vATime])
|
||||
v_name = it[VideoTable.vName],
|
||||
v_a_time = formatLocalDateTimeToString(it[VideoTable.vATime])
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getAnalyticsDetailByVideoId(vId: Int): ResultRow? {
|
||||
return transaction {
|
||||
VideoTable.selectAll().where { VideoTable.id eq vId }.singleOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
fun selectVideoDetailByVid(vId:Int): List<VideoDetail>{
|
||||
fun selectVideoDetailByVid(vId: Int): List<VideoDetail> {
|
||||
return transaction {
|
||||
VideoAnalyticsDetailTable
|
||||
.selectAll()
|
||||
+5
-5
@@ -1,12 +1,12 @@
|
||||
package ink.snowflake.server.model.database
|
||||
package ink.snowflake.server.database.table
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.json.json
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import ink.snowflake.server.gson
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||
import org.jetbrains.exposed.v1.json.json
|
||||
|
||||
// 定义 AI 表
|
||||
object AIProfilesTable : IntIdTable("ai_profiles") {
|
||||
+5
-6
@@ -1,10 +1,9 @@
|
||||
package ink.snowflake.server.model.database
|
||||
package ink.snowflake.server.database.table
|
||||
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.Column
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||
|
||||
object ChatRecordsTable : IntIdTable("chat_records") {
|
||||
// 用户 ID(外键)
|
||||
+4
-5
@@ -1,11 +1,10 @@
|
||||
package ink.snowflake.server.model.database
|
||||
package ink.snowflake.server.database.table
|
||||
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import ink.snowflake.server.gson
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.json.json
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
||||
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)
|
||||
+6
-5
@@ -1,9 +1,7 @@
|
||||
package ink.snowflake.server.model.database
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||
import java.time.LocalDateTime
|
||||
package ink.snowflake.server.database.table
|
||||
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||
|
||||
object UserTable : IntIdTable("users") {
|
||||
// 用户名
|
||||
@@ -26,4 +24,7 @@ object UserTable : IntIdTable("users") {
|
||||
|
||||
// 更新时间
|
||||
val updatedAt = timestamp("updated_at").nullable()
|
||||
|
||||
// 角色
|
||||
val roles = text("roles").nullable()
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package ink.snowflake.server.model.database
|
||||
package ink.snowflake.server.database.table
|
||||
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
|
||||
|
||||
object VideoAnalyticsDetailTable : IntIdTable("video_analytics_detail") {
|
||||
+3
-4
@@ -1,8 +1,7 @@
|
||||
package ink.snowflake.server.model.database
|
||||
package ink.snowflake.server.database.table
|
||||
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.datetime.datetime
|
||||
|
||||
|
||||
object VideoTable : IntIdTable("video") {
|
||||
@@ -0,0 +1,19 @@
|
||||
package ink.snowflake.server.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
import java.util.UUID
|
||||
|
||||
object UUIDSerializer : KSerializer<UUID> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: UUID) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): UUID {
|
||||
return UUID.fromString(decoder.decodeString())
|
||||
}
|
||||
}
|
||||
+2
-4
@@ -1,4 +1,4 @@
|
||||
package ink.snowflake.server.model.ai
|
||||
package ink.snowflake.server.model.request
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@@ -16,6 +16,4 @@ data class ChatRequest(
|
||||
@SerializedName("role")
|
||||
val role: String = "user"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package ink.snowflake.server.model.database
|
||||
package ink.snowflake.server.model.request
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -15,4 +15,4 @@ data class ImageAnalyticsRequest(
|
||||
val average_confidence: Float, // 平均置信度
|
||||
val other_info: Map<String, String>, // 额外信息
|
||||
val processing_time: String // 处理时间
|
||||
)
|
||||
)
|
||||
+2
-4
@@ -1,4 +1,4 @@
|
||||
package ink.snowflake.server.model.ai
|
||||
package ink.snowflake.server.model.response
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@@ -32,6 +32,4 @@ data class ChatResponse(
|
||||
@SerializedName("role")
|
||||
val role: String = ""
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class VideoAnalyticsDetail(
|
||||
val v_id: Int,
|
||||
val v_name: String,
|
||||
val v_video_play_path: String,
|
||||
val v_file_name: String,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ink.snowflake.server.plugins
|
||||
|
||||
import ink.snowflake.server.utils.AppConfig
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.v1.jdbc.Database
|
||||
|
||||
fun configureDatabases(config: AppConfig) {
|
||||
Database.connect(
|
||||
|
||||
@@ -6,8 +6,6 @@ import io.ktor.server.auth.*
|
||||
import io.ktor.server.http.content.*
|
||||
import io.ktor.server.plugins.autohead.AutoHeadResponse
|
||||
import io.ktor.server.plugins.partialcontent.PartialContent
|
||||
import io.ktor.server.routing.*
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import java.io.File
|
||||
|
||||
fun Application.configureStaticPath() {
|
||||
|
||||
@@ -72,7 +72,8 @@ fun Application.configureStatusPages() {
|
||||
status(HttpStatusCode.InternalServerError) { call, status ->
|
||||
val response = BaseResponse<Nothing>(
|
||||
status = false,
|
||||
message = "${status.value} Internal Server Error," + status.description
|
||||
// message = "${status.value} Internal Server Error" + status.description
|
||||
message = "${status.value} Internal Server Error"
|
||||
)
|
||||
call.respond(status, response)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ink.snowflake.server.route
|
||||
package ink.snowflake.server.plugins
|
||||
|
||||
import io.ktor.serialization.gson.*
|
||||
import io.ktor.server.application.*
|
||||
@@ -1,32 +0,0 @@
|
||||
package ink.snowflake.server.repository
|
||||
|
||||
import ink.snowflake.server.model.database.ImageAnalyticsRequest
|
||||
import ink.snowflake.server.model.database.VideoTable
|
||||
import ink.snowflake.server.model.database.VideoTable.vAAverageMaskedRatio
|
||||
import ink.snowflake.server.model.database.VideoTable.vACountPeople
|
||||
import ink.snowflake.server.model.database.VideoTable.vAMaxAction
|
||||
import ink.snowflake.server.model.database.VideoTable.vAMaxStayTime
|
||||
import ink.snowflake.server.model.database.VideoTable.vATotalPeople
|
||||
import ink.snowflake.server.model.database.VideoTable.vStartDateTime
|
||||
import ink.snowflake.server.model.response.Area
|
||||
import ink.snowflake.server.model.response.DetailItem
|
||||
import ink.snowflake.server.model.response.ItemStyle
|
||||
import ink.snowflake.server.model.response.VideoAnalyticsData
|
||||
import ink.snowflake.server.model.response.VideoAnalyticsDetail
|
||||
import ink.snowflake.server.model.response.VideoListResponse
|
||||
import ink.snowflake.server.utils.database.ImageDao
|
||||
import ink.snowflake.server.utils.database.VideoDao
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.toJavaLocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class ImageDataBase {
|
||||
|
||||
fun saveImageAnalyticsData(request: ImageAnalyticsRequest) {
|
||||
return ImageDao.insertImageAnalyticsData(request)
|
||||
}
|
||||
|
||||
fun getImageList(): List<ImageAnalyticsRequest> {
|
||||
return ImageDao.getVideoList()
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package ink.snowflake.server.repository
|
||||
|
||||
import ink.snowflake.server.utils.database.UserDAO
|
||||
import java.security.MessageDigest
|
||||
import kotlin.text.Charsets.UTF_8
|
||||
|
||||
class UserDataBase {
|
||||
|
||||
fun login(email: String, password: String): Int {
|
||||
// 查找用户
|
||||
val userId = UserDAO.getUserIdByEmail(email)
|
||||
return if(userId == null){
|
||||
// 账号不存在
|
||||
-1
|
||||
}else{
|
||||
val userPassword = UserDAO.getPasswordById(userId)
|
||||
// 验证密码
|
||||
if (password == userPassword) {
|
||||
// 登录成功
|
||||
userId
|
||||
} else {
|
||||
// 账号密码不匹配
|
||||
-2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 正数:userId 负数:错误码
|
||||
*/
|
||||
fun registerByEmail(email: String, password: String): Int {
|
||||
// 查找用户
|
||||
val userId = UserDAO.getUserIdByEmail(email)
|
||||
return if(userId != null){
|
||||
// 如果用户已存在,返回 0
|
||||
0
|
||||
}else{
|
||||
// 用户不存在,插入新用户
|
||||
UserDAO.registerByEmailAndGetId(email, password)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun hashPassword(password: String): String {
|
||||
val bytes = MessageDigest.getInstance("SHA-256").digest(password.toByteArray(UTF_8))
|
||||
return bytes.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package ink.snowflake.server.repository
|
||||
|
||||
import ink.snowflake.server.SERVER_PATH
|
||||
import ink.snowflake.server.model.database.VideoTable
|
||||
import ink.snowflake.server.model.database.VideoTable.vAAverageMaskedRatio
|
||||
import ink.snowflake.server.model.database.VideoTable.vACountPeople
|
||||
import ink.snowflake.server.model.database.VideoTable.vAMaxAction
|
||||
import ink.snowflake.server.model.database.VideoTable.vAMaxStayTime
|
||||
import ink.snowflake.server.model.database.VideoTable.vATotalPeople
|
||||
import ink.snowflake.server.model.database.VideoTable.vStartDateTime
|
||||
import ink.snowflake.server.model.response.Area
|
||||
import ink.snowflake.server.model.response.DetailItem
|
||||
import ink.snowflake.server.model.response.ItemStyle
|
||||
import ink.snowflake.server.model.response.VideoAnalyticsData
|
||||
import ink.snowflake.server.model.response.VideoAnalyticsDetail
|
||||
import ink.snowflake.server.model.request.VideoAnalyticsRequest
|
||||
import ink.snowflake.server.model.response.VideoListResponse
|
||||
import ink.snowflake.server.utils.database.VideoDao
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.toJavaLocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class VideoDataBase {
|
||||
|
||||
fun saveVideoAnalyticsData(request: VideoAnalyticsRequest) {
|
||||
return VideoDao.insertVideoAnalyticsData(request)
|
||||
}
|
||||
|
||||
fun getVideoList(): List<VideoListResponse> {
|
||||
return VideoDao.getVideoList()
|
||||
}
|
||||
|
||||
fun getAnalyticsDetailByVideoId(vId: Int): VideoAnalyticsDetail? {
|
||||
// 查询视频信息
|
||||
val video = VideoDao.getAnalyticsDetailByVideoId(vId)
|
||||
if (video == null) {
|
||||
return null
|
||||
}
|
||||
// 查询相关的分析详情
|
||||
val details = VideoDao.selectVideoDetailByVid(vId)
|
||||
|
||||
// 用于返回的数据
|
||||
val yTotalData = mutableListOf<Pair<String, Int>>() // (时间, 总人数)
|
||||
val yMaskedData = mutableListOf<Pair<String, Int>>() // (时间, 佩戴口罩人数)
|
||||
val areaData = mutableListOf<List<Area>>()
|
||||
val detailList = mutableListOf<DetailItem>()
|
||||
// 颜色映射
|
||||
val colors = mapOf(
|
||||
"feed" to "rgba(0, 255, 0, 0.4)", // 淡绿色
|
||||
"disinfection" to "rgba(0, 0, 255, 0.4)", // 淡蓝色
|
||||
"other" to "rgba(255, 0, 0, 0.4)" // 淡红色
|
||||
)
|
||||
// 生成 yTotalData 和 yMaskedData,同时计算 areaData
|
||||
var lastAction: String? = null
|
||||
var areaStartTime: String? = null
|
||||
var currentColor: String? = null
|
||||
|
||||
for (detail in details) {
|
||||
// 获取视频开始时间
|
||||
val vStartDatetime: LocalDateTime = video[vStartDateTime]!!
|
||||
val timeStr = vStartDatetime.toJavaLocalDateTime()
|
||||
.plusSeconds(detail.a_time_stamp.toLong())
|
||||
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
|
||||
|
||||
// 添加总人数和口罩佩戴人数
|
||||
yTotalData.add(timeStr to detail.a_total_people)
|
||||
yMaskedData.add(timeStr to detail.a_total_people_masked)
|
||||
|
||||
// 处理 areaData,根据 a_action 判断是否需要创建新的区域
|
||||
if (detail.a_action != "--") { // 只处理非 "--" 动作
|
||||
if (lastAction == null || lastAction != detail.a_action) {
|
||||
// 如果上一个动作和当前动作不同,并且 areaStartTime 已经存在,创建新的区域
|
||||
if (areaStartTime != null) {
|
||||
areaData.add(
|
||||
listOf(
|
||||
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
|
||||
Area(timeStr, ItemStyle(currentColor ?: "#FF0000"))
|
||||
)
|
||||
)
|
||||
}
|
||||
// 添加到 detailList,记录动作开始的时刻
|
||||
detailList.add(
|
||||
DetailItem(getFriendlyActionName(detail.a_action), time = timeStr)
|
||||
)
|
||||
|
||||
// 更新为新的动作
|
||||
lastAction = detail.a_action
|
||||
areaStartTime = timeStr
|
||||
currentColor = colors[detail.a_action] ?: "#FF0000" // 默认红色
|
||||
}
|
||||
} else {
|
||||
// 如果当前动作为 "--",则结束当前区域,并重置状态
|
||||
if (areaStartTime != null) {
|
||||
// 添加到 detailList,记录动作开始的时刻
|
||||
detailList.add(
|
||||
DetailItem(getFriendlyActionName(detail.a_action), timeStr)
|
||||
)
|
||||
// 结束当前区域
|
||||
areaData.add(
|
||||
listOf(
|
||||
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
|
||||
Area(timeStr, ItemStyle(currentColor ?: "#FF0000"))
|
||||
)
|
||||
)
|
||||
// 重置状态
|
||||
lastAction = null
|
||||
areaStartTime = null
|
||||
currentColor = null
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理最后一个区域的结束时间
|
||||
if (areaStartTime != null && lastAction != null) {
|
||||
areaData.add(
|
||||
listOf(
|
||||
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
|
||||
Area(yTotalData.last().first, ItemStyle(currentColor ?: "#FF0000"))
|
||||
)
|
||||
)
|
||||
}
|
||||
val analyticsData = VideoAnalyticsData(
|
||||
yTotalData = yTotalData,
|
||||
yMaskedData = yMaskedData,
|
||||
areaData = areaData
|
||||
)
|
||||
// 返回数据
|
||||
return VideoAnalyticsDetail(
|
||||
v_name = video[VideoTable.vName],
|
||||
v_video_play_path = "http://${SERVER_PATH}:9000/video/" + video[VideoTable.vObjectName],
|
||||
v_file_name = video[VideoTable.vFileName],
|
||||
v_duration = video[VideoTable.vDuration],
|
||||
v_size = video[VideoTable.vSize],
|
||||
v_start_datetime = video[VideoTable.vStartDateTime]?.toString() ?: "",
|
||||
v_video_codec = video[VideoTable.vVideoCodec],
|
||||
v_audio_codec = video[VideoTable.vAudioCodec],
|
||||
v_overall_bit_rate = video[VideoTable.vOverallBitRate],
|
||||
v_resolution = video[VideoTable.vResolution],
|
||||
v_a_time = video[VideoTable.vATime].toString(),
|
||||
v_a_total_people = video[vATotalPeople], // 总人数
|
||||
v_a_count_people = video[vACountPeople], // 佩戴口罩人数
|
||||
v_a_max_stay_time = video[vAMaxStayTime], // 最大停留时间
|
||||
v_a_max_action = getFriendlyActionName(video[vAMaxAction]), // 最大动作
|
||||
v_a_average_masked_ratio = video[vAAverageMaskedRatio], // 平均佩戴口罩比例
|
||||
v_a_details = analyticsData, // 这里是计算得来的 VideoAnalyticsData
|
||||
v_details_list = detailList
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFriendlyActionName(name: String): String {
|
||||
return if (name == "feed") {
|
||||
"喂桑"
|
||||
} else if (name == "disinfection") {
|
||||
"消毒"
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
package ink.snowflake.server.route.func
|
||||
package ink.snowflake.server.route
|
||||
|
||||
import com.cyberecho.mdoel.database.WSChatRecords
|
||||
import ink.snowflake.server.gson
|
||||
import ink.snowflake.server.model.ai.ChatRequest
|
||||
import ink.snowflake.server.model.ai.ChatResponse
|
||||
import ink.snowflake.server.model.request.ChatRequest
|
||||
import ink.snowflake.server.model.response.ChatResponse
|
||||
import ink.snowflake.server.model.response.BaseResponse
|
||||
import ink.snowflake.server.utils.WebSocketManager
|
||||
import ink.snowflake.server.utils.database.ChatRecordsDao
|
||||
import ink.snowflake.server.database.ChatRecordsDao
|
||||
import ink.snowflake.server.utils.getUserIdByToken
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
+5
-8
@@ -1,24 +1,21 @@
|
||||
package ink.snowflake.server.route.func
|
||||
package ink.snowflake.server.route
|
||||
|
||||
import ink.snowflake.server.model.database.ImageAnalyticsRequest
|
||||
import ink.snowflake.server.model.request.VideoAnalyticsRequest
|
||||
import ink.snowflake.server.model.request.ImageAnalyticsRequest
|
||||
import ink.snowflake.server.model.response.*
|
||||
import ink.snowflake.server.repository.ImageDataBase
|
||||
import ink.snowflake.server.database.ImageDao
|
||||
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.websocket.*
|
||||
|
||||
fun Application.ImageAnalytics() {
|
||||
val repository = ImageDataBase()
|
||||
routing {
|
||||
route("/api") {
|
||||
// 上传分析结果
|
||||
post("/saveImageAnalyticsData") {
|
||||
val request = call.receive<ImageAnalyticsRequest>()
|
||||
call.respond(BaseResponse(data = repository.saveImageAnalyticsData(request)))
|
||||
call.respond(BaseResponse(data = ImageDao.insertImageAnalyticsData(request)))
|
||||
}
|
||||
authenticate {
|
||||
// 拍照保存为图片 并且调用Python程序进行分析
|
||||
@@ -33,7 +30,7 @@ fun Application.ImageAnalytics() {
|
||||
}
|
||||
// 获取已分析视频列表
|
||||
get("/getImageList") {
|
||||
val res = repository.getImageList()
|
||||
val res = ImageDao.getVideoList()
|
||||
call.respond(BaseResponse(data = res))
|
||||
}
|
||||
}
|
||||
+3
-6
@@ -1,7 +1,7 @@
|
||||
package ink.snowflake.server.route.func
|
||||
package ink.snowflake.server.route
|
||||
|
||||
import com.google.gson.Gson
|
||||
import ink.snowflake.server.SERVER_PATH
|
||||
import ink.snowflake.server.SERVER_PATH_FRP
|
||||
import ink.snowflake.server.model.request.DevicesInfo
|
||||
import ink.snowflake.server.model.response.*
|
||||
import ink.snowflake.server.utils.runCommand
|
||||
@@ -13,14 +13,11 @@ import io.ktor.client.plugins.auth.providers.basic
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.content.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.authenticate
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.websocket.*
|
||||
import io.ktor.utils.io.*
|
||||
import io.ktor.websocket.*
|
||||
import io.ktor.websocket.send
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
@@ -59,7 +56,7 @@ fun Application.RemoteDebug() {
|
||||
get("/connectLocalDevice") {
|
||||
val port = call.parameters["port"]
|
||||
if (port != null) {
|
||||
val result = runAdbCommand("connect ${SERVER_PATH}:$port")
|
||||
val result = runAdbCommand("connect ${SERVER_PATH_FRP}:$port")
|
||||
call.respond(BaseResponse(data = result))
|
||||
} else {
|
||||
call.respond(BaseResponse(status = false, message = "IP或端口无效", data = null))
|
||||
+46
-20
@@ -1,16 +1,14 @@
|
||||
package ink.snowflake.server.route.func
|
||||
package ink.snowflake.server.route
|
||||
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import ink.snowflake.server.utils.database.AIDao
|
||||
import ink.snowflake.server.model.request.CommonRequest
|
||||
import ink.snowflake.server.model.request.LoginRequest
|
||||
import ink.snowflake.server.model.request.RefreshTokenRequest
|
||||
import ink.snowflake.server.model.request.RegisterRequest
|
||||
import ink.snowflake.server.model.response.*
|
||||
import ink.snowflake.server.repository.UserDataBase
|
||||
import ink.snowflake.server.utils.AppConfig
|
||||
import ink.snowflake.server.utils.database.UserDAO
|
||||
import ink.snowflake.server.database.UserDAO
|
||||
import ink.snowflake.server.utils.getUserIdByToken
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
@@ -22,12 +20,15 @@ import org.redisson.Redisson
|
||||
import org.redisson.api.RBucket
|
||||
import org.redisson.api.RedissonClient
|
||||
import org.redisson.config.Config
|
||||
import java.security.MessageDigest
|
||||
import java.text.DateFormat
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import javax.mail.*
|
||||
import javax.mail.internet.InternetAddress
|
||||
import javax.mail.internet.MimeMessage
|
||||
import kotlin.collections.joinToString
|
||||
import kotlin.text.Charsets.UTF_8
|
||||
|
||||
// 配置和初始化 Redis 客户端
|
||||
fun setupRedis(): RedissonClient {
|
||||
@@ -37,7 +38,6 @@ fun setupRedis(): RedissonClient {
|
||||
}
|
||||
|
||||
fun Application.User(config: AppConfig) {
|
||||
val repository = UserDataBase()
|
||||
// 初始化 Redis 连接
|
||||
val redisClient: RedissonClient = setupRedis()
|
||||
routing {
|
||||
@@ -47,21 +47,33 @@ fun Application.User(config: AppConfig) {
|
||||
val loginRequest = call.receive<LoginRequest>()
|
||||
val email = loginRequest.account
|
||||
val password = loginRequest.password
|
||||
val userId = repository.login(email, password)
|
||||
call.respond(
|
||||
if (userId == -1) {
|
||||
BaseResponse(status = false, message = "尚未注册", data = null)
|
||||
} else if (userId == -2) {
|
||||
BaseResponse(status = false, message = "账号密码不匹配,请重新登录", data = null)
|
||||
// 查找用户
|
||||
val userId = UserDAO.getUserIdByEmail(email)
|
||||
val res = if(userId == null){
|
||||
// 账号不存在
|
||||
-1
|
||||
}else{
|
||||
val userPassword = UserDAO.getPasswordById(userId)
|
||||
// 验证密码
|
||||
if (password == userPassword) {
|
||||
// 登录成功
|
||||
userId
|
||||
} else {
|
||||
BaseResponse(
|
||||
// 账号密码不匹配
|
||||
-2
|
||||
}
|
||||
}
|
||||
call.respond(
|
||||
when (res) {
|
||||
-1 -> BaseResponse(status = false, message = "尚未注册", data = null)
|
||||
-2 -> BaseResponse(status = false, message = "账号密码不匹配,请重新登录", data = null)
|
||||
else -> BaseResponse(
|
||||
status = true, data = LoginResponse(
|
||||
userId.toString(),
|
||||
generateAccessToken(config, userId),
|
||||
generateRefreshToken(config, userId)
|
||||
generateAccessToken(config, res),
|
||||
generateRefreshToken(config, res)
|
||||
)
|
||||
)
|
||||
// match == -1
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -91,16 +103,24 @@ fun Application.User(config: AppConfig) {
|
||||
} else if (storedCode != register.code) {
|
||||
call.respond(BaseResponse(status = false, message = "验证码错误", data = null))
|
||||
} else {
|
||||
val userId = repository.registerByEmail(register.account, register.password)
|
||||
if (userId > 0) {
|
||||
// 查找用户
|
||||
val userId = UserDAO.getUserIdByEmail(register.account)
|
||||
val res = if(userId != null){
|
||||
// 如果用户已存在,返回 0
|
||||
0
|
||||
}else{
|
||||
// 用户不存在,插入新用户
|
||||
UserDAO.registerByEmailAndGetId(register.account,register.password)
|
||||
}
|
||||
if (res > 0) {
|
||||
call.respond(
|
||||
BaseResponse(
|
||||
status = true, message = "注册成功", data =
|
||||
BaseResponse(
|
||||
status = true, data = LoginResponse(
|
||||
account,
|
||||
generateAccessToken(config, userId),
|
||||
generateRefreshToken(config, userId)
|
||||
generateAccessToken(config, res),
|
||||
generateRefreshToken(config, res)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -253,4 +273,10 @@ fun sendVerificationEmail(config: AppConfig, recipientEmail: String, verificatio
|
||||
e.printStackTrace()
|
||||
println("Failed to send verification email.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun hashPassword(password: String): String {
|
||||
val bytes = MessageDigest.getInstance("SHA-256").digest(password.toByteArray(UTF_8))
|
||||
return bytes.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package ink.snowflake.server.route
|
||||
|
||||
import ink.snowflake.server.SERVER_PATH_OSS
|
||||
import ink.snowflake.server.VIDEO_INPUT_PATH
|
||||
import ink.snowflake.server.database.table.VideoTable
|
||||
import ink.snowflake.server.database.table.VideoTable.vAAverageMaskedRatio
|
||||
import ink.snowflake.server.database.table.VideoTable.vACountPeople
|
||||
import ink.snowflake.server.database.table.VideoTable.vAMaxAction
|
||||
import ink.snowflake.server.database.table.VideoTable.vAMaxStayTime
|
||||
import ink.snowflake.server.database.table.VideoTable.vATotalPeople
|
||||
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.websocket.*
|
||||
import io.ktor.websocket.*
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.toJavaLocalDateTime
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
|
||||
val clients = Collections.synchronizedList<WebSocketServerSession>(ArrayList()) // 线程安全的客户端列表
|
||||
var aiState = "等待分析任务中"
|
||||
|
||||
fun Application.VideoAnalytics() {
|
||||
routing {
|
||||
// 实时发送AI状态
|
||||
webSocket("/handleState") { // WebSocket 路由
|
||||
clients.add(this) // 添加当前连接的客户端
|
||||
send(aiState) // 向客户端发送连接成功消息
|
||||
try {
|
||||
incoming.consumeEach { frame -> // 持续接收消息
|
||||
when (frame) {
|
||||
is Frame.Text -> {
|
||||
aiState = frame.readText() // 更新状态
|
||||
broadcastMessage(aiState) // 使用封装的方法广播消息
|
||||
}
|
||||
|
||||
is Frame.Close -> {
|
||||
println("Closed")
|
||||
clients.remove(this)
|
||||
close() // 确保关闭 WebSocket 连接
|
||||
return@consumeEach
|
||||
}
|
||||
// 其他消息类型的处理
|
||||
is Frame.Binary -> TODO() // 处理二进制消息
|
||||
is Frame.Ping -> TODO() // 处理 Ping 消息
|
||||
is Frame.Pong -> TODO() // 处理 Pong 消息
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 处理接收消息时的异常
|
||||
close(CloseReason(CloseReason.Codes.NORMAL, "Client disconnected"))
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
clients.remove(this) // 确保在连接关闭时移除客户端
|
||||
}
|
||||
}
|
||||
route("/api") {
|
||||
route("/iva") {
|
||||
// 上传分析结果
|
||||
post("/saveVideoAnalyticsData") {
|
||||
val request = call.receive<VideoAnalyticsRequest>()
|
||||
// todo 上传这里未做测试
|
||||
call.respond(BaseResponse(data = VideoDao.insertVideoAnalyticsData(request)))
|
||||
}
|
||||
authenticate {
|
||||
post("/upload") {
|
||||
val multipart = call.receiveMultipart() //1G
|
||||
// 确保 uploads 目录存在
|
||||
val uploadDir = File(VIDEO_INPUT_PATH)
|
||||
if (!uploadDir.exists()) {
|
||||
if (!uploadDir.mkdirs()) {
|
||||
println("无法创建目录")
|
||||
throw IOException("Failed to create upload directory.")
|
||||
}
|
||||
}
|
||||
var fileName = ""
|
||||
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
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is PartData.FormItem -> {
|
||||
when (part.name) {
|
||||
"name" -> name = part.value
|
||||
"datetime" -> datetime = part.value
|
||||
}
|
||||
}
|
||||
|
||||
else -> part.dispose()
|
||||
}
|
||||
}
|
||||
call.respond(BaseResponse(message = "上传成功", data = null))
|
||||
broadcastMessage("上传完成,开始启动AI引擎")
|
||||
val command = listOf(
|
||||
"/usr/bin/python3",
|
||||
"/home/xhcp/mine/IntelligentVideoAnalytics/AI_Project/DeepStream_Action_Recognition/core/final.py",
|
||||
"$VIDEO_INPUT_PATH$fileName",
|
||||
datetime,
|
||||
name
|
||||
)
|
||||
println("-----------------" + command.joinToString(" "))
|
||||
runCommand(command) {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
// 获取已分析视频列表
|
||||
get("/getVideoList") {
|
||||
val name = call.parameters["name"]
|
||||
val res = VideoDao.getVideoList(name)
|
||||
call.respond(BaseResponse(data = res))
|
||||
}
|
||||
// 获取某视频分析详情
|
||||
get("/getAnalyticsDetailByVideoId") {
|
||||
// 获取 vId 参数
|
||||
val vIdParam = call.parameters["vId"]
|
||||
val vId = vIdParam?.toIntOrNull() // 将 vId 转换为 Int,确保安全
|
||||
if (vId == null) {
|
||||
call.respond(BaseResponse(status = true, message = "Invalid vId", data = null))
|
||||
return@get
|
||||
}
|
||||
// 查询视频信息
|
||||
val video = VideoDao.getAnalyticsDetailByVideoId(vId)
|
||||
if (video == null) {
|
||||
call.respond(BaseResponse(status = false, message = "不存在该视频", data = null))
|
||||
} else {
|
||||
// 查询相关的分析详情
|
||||
val details = VideoDao.selectVideoDetailByVid(vId)
|
||||
|
||||
// 用于返回的数据
|
||||
val yTotalData = mutableListOf<Pair<String, Int>>() // (时间, 总人数)
|
||||
val yMaskedData = mutableListOf<Pair<String, Int>>() // (时间, 佩戴口罩人数)
|
||||
val areaData = mutableListOf<List<Area>>()
|
||||
val detailList = mutableListOf<DetailItem>()
|
||||
// 颜色映射
|
||||
val colors = mapOf(
|
||||
"feed" to "rgba(0, 255, 0, 0.4)", // 淡绿色
|
||||
"disinfection" to "rgba(0, 0, 255, 0.4)", // 淡蓝色
|
||||
"other" to "rgba(255, 0, 0, 0.4)" // 淡红色
|
||||
)
|
||||
// 生成 yTotalData 和 yMaskedData,同时计算 areaData
|
||||
var lastAction: String? = null
|
||||
var areaStartTime: String? = null
|
||||
var currentColor: String? = null
|
||||
|
||||
for (detail in details) {
|
||||
// 获取视频开始时间
|
||||
val vStartDatetime: LocalDateTime = video[vStartDateTime]!!
|
||||
val timeStr = vStartDatetime.toJavaLocalDateTime()
|
||||
.plusSeconds(detail.a_time_stamp.toLong())
|
||||
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
|
||||
|
||||
// 添加总人数和口罩佩戴人数
|
||||
yTotalData.add(timeStr to detail.a_total_people)
|
||||
yMaskedData.add(timeStr to detail.a_total_people_masked)
|
||||
|
||||
// 处理 areaData,根据 a_action 判断是否需要创建新的区域
|
||||
if (detail.a_action != "--") { // 只处理非 "--" 动作
|
||||
if (lastAction == null || lastAction != detail.a_action) {
|
||||
// 如果上一个动作和当前动作不同,并且 areaStartTime 已经存在,创建新的区域
|
||||
if (areaStartTime != null) {
|
||||
areaData.add(
|
||||
listOf(
|
||||
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
|
||||
Area(timeStr, ItemStyle(currentColor ?: "#FF0000"))
|
||||
)
|
||||
)
|
||||
}
|
||||
// 添加到 detailList,记录动作开始的时刻
|
||||
detailList.add(
|
||||
DetailItem(getFriendlyActionName(detail.a_action), time = timeStr)
|
||||
)
|
||||
|
||||
// 更新为新的动作
|
||||
lastAction = detail.a_action
|
||||
areaStartTime = timeStr
|
||||
currentColor = colors[detail.a_action] ?: "#FF0000" // 默认红色
|
||||
}
|
||||
} else {
|
||||
// 如果当前动作为 "--",则结束当前区域,并重置状态
|
||||
if (areaStartTime != null) {
|
||||
// 添加到 detailList,记录动作开始的时刻
|
||||
detailList.add(
|
||||
DetailItem(getFriendlyActionName(detail.a_action), timeStr)
|
||||
)
|
||||
// 结束当前区域
|
||||
areaData.add(
|
||||
listOf(
|
||||
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
|
||||
Area(timeStr, ItemStyle(currentColor ?: "#FF0000"))
|
||||
)
|
||||
)
|
||||
// 重置状态
|
||||
lastAction = null
|
||||
areaStartTime = null
|
||||
currentColor = null
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理最后一个区域的结束时间
|
||||
if (areaStartTime != null && lastAction != null) {
|
||||
areaData.add(
|
||||
listOf(
|
||||
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
|
||||
Area(yTotalData.last().first, ItemStyle(currentColor ?: "#FF0000"))
|
||||
)
|
||||
)
|
||||
}
|
||||
val analyticsData = VideoAnalyticsData(
|
||||
yTotalData = yTotalData,
|
||||
yMaskedData = yMaskedData,
|
||||
areaData = areaData
|
||||
)
|
||||
// 返回数据
|
||||
call.respond(
|
||||
BaseResponse(
|
||||
data = VideoAnalyticsDetail(
|
||||
v_id = vId,
|
||||
v_name = video[VideoTable.vName],
|
||||
v_video_play_path = "http://${SERVER_PATH_OSS}:9000/video/" + video[VideoTable.vObjectName],
|
||||
v_file_name = video[VideoTable.vFileName],
|
||||
v_duration = video[VideoTable.vDuration],
|
||||
v_size = video[VideoTable.vSize],
|
||||
v_start_datetime = video[vStartDateTime]?.toString() ?: "",
|
||||
v_video_codec = video[VideoTable.vVideoCodec],
|
||||
v_audio_codec = video[VideoTable.vAudioCodec],
|
||||
v_overall_bit_rate = video[VideoTable.vOverallBitRate],
|
||||
v_resolution = video[VideoTable.vResolution],
|
||||
v_a_time = video[VideoTable.vATime].toString(),
|
||||
v_a_total_people = video[vATotalPeople], // 总人数
|
||||
v_a_count_people = video[vACountPeople], // 佩戴口罩人数
|
||||
v_a_max_stay_time = video[vAMaxStayTime], // 最大停留时间
|
||||
v_a_max_action = getFriendlyActionName(video[vAMaxAction]), // 最大动作
|
||||
v_a_average_masked_ratio = video[vAAverageMaskedRatio], // 平均佩戴口罩比例
|
||||
v_a_details = analyticsData, // 这里是计算得来的 VideoAnalyticsData
|
||||
v_details_list = detailList
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun broadcastMessage(message: String) { // 封装的广播消息方法
|
||||
println("发送消息:$message")
|
||||
clients.forEach { client ->
|
||||
try {
|
||||
client.send(message) // 发送消息到每个客户端
|
||||
} catch (e: Exception) {
|
||||
println("发送消息给客户端时出错,移除客户端: ${e.message}")
|
||||
clients.remove(client) // 移除已断开的客户端
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFriendlyActionName(name: String): String {
|
||||
return if (name == "feed") {
|
||||
"喂桑"
|
||||
} else if (name == "disinfection") {
|
||||
"消毒"
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ink.snowflake.server.route.func
|
||||
package ink.snowflake.server.route
|
||||
|
||||
import ink.snowflake.server.model.response.*
|
||||
import io.ktor.server.application.*
|
||||
@@ -1,159 +0,0 @@
|
||||
package ink.snowflake.server.route.func
|
||||
|
||||
import ink.snowflake.server.VIDEO_INPUT_PATH
|
||||
import ink.snowflake.server.model.request.VideoAnalyticsRequest
|
||||
import ink.snowflake.server.model.response.*
|
||||
import ink.snowflake.server.repository.VideoDataBase
|
||||
import ink.snowflake.server.utils.WebSocketManager.broadcastMessage
|
||||
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.websocket.*
|
||||
import io.ktor.websocket.*
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
val clients = Collections.synchronizedList<WebSocketServerSession>(ArrayList()) // 线程安全的客户端列表
|
||||
var aiState = "等待分析任务中"
|
||||
|
||||
fun Application.VideoAnalytics() {
|
||||
val repository = VideoDataBase()
|
||||
routing {
|
||||
// 实时发送AI状态
|
||||
webSocket("/handleState") { // WebSocket 路由
|
||||
clients.add(this) // 添加当前连接的客户端
|
||||
send(aiState) // 向客户端发送连接成功消息
|
||||
try {
|
||||
incoming.consumeEach { frame -> // 持续接收消息
|
||||
when (frame) {
|
||||
is Frame.Text -> {
|
||||
aiState = frame.readText() // 更新状态
|
||||
broadcastMessage(aiState) // 使用封装的方法广播消息
|
||||
}
|
||||
|
||||
is Frame.Close -> {
|
||||
println("Closed")
|
||||
clients.remove(this)
|
||||
close() // 确保关闭 WebSocket 连接
|
||||
return@consumeEach
|
||||
}
|
||||
// 其他消息类型的处理
|
||||
is Frame.Binary -> TODO() // 处理二进制消息
|
||||
is Frame.Ping -> TODO() // 处理 Ping 消息
|
||||
is Frame.Pong -> TODO() // 处理 Pong 消息
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 处理接收消息时的异常
|
||||
close(CloseReason(CloseReason.Codes.NORMAL, "Client disconnected"))
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
clients.remove(this) // 确保在连接关闭时移除客户端
|
||||
}
|
||||
}
|
||||
route("/api") {
|
||||
// 上传分析结果
|
||||
post("/saveVideoAnalyticsData") {
|
||||
val request = call.receive<VideoAnalyticsRequest>()
|
||||
// todo 上传这里未做测试
|
||||
call.respond(BaseResponse(data = repository.saveVideoAnalyticsData(request)))
|
||||
}
|
||||
authenticate {
|
||||
post("/upload") {
|
||||
val multipart = call.receiveMultipart() //1G
|
||||
// 确保 uploads 目录存在
|
||||
val uploadDir = File(VIDEO_INPUT_PATH)
|
||||
if (!uploadDir.exists()) {
|
||||
if (!uploadDir.mkdirs()) {
|
||||
println("无法创建目录")
|
||||
throw IOException("Failed to create upload directory.")
|
||||
}
|
||||
}
|
||||
var fileName = ""
|
||||
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
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is PartData.FormItem -> {
|
||||
when (part.name) {
|
||||
"name" -> name = part.value
|
||||
"datetime" -> datetime = part.value
|
||||
}
|
||||
}
|
||||
|
||||
else -> part.dispose()
|
||||
}
|
||||
}
|
||||
call.respond(BaseResponse(message = "上传成功", data = null))
|
||||
broadcastMessage("上传完成,开始启动AI引擎")
|
||||
val command = listOf(
|
||||
"/usr/bin/python3",
|
||||
"/home/xhcp/mine/IntelligentVideoAnalytics/AI_Project/DeepStream_Action_Recognition/core/final.py",
|
||||
"$VIDEO_INPUT_PATH$fileName",
|
||||
datetime,
|
||||
name
|
||||
)
|
||||
println("-----------------" + command.joinToString(" "))
|
||||
runCommand(command) {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
// 获取已分析视频列表
|
||||
get("/getVideoList") {
|
||||
val res = repository.getVideoList()
|
||||
call.respond(BaseResponse(data = res))
|
||||
}
|
||||
// 获取某视频分析详情
|
||||
get("/getAnalyticsDetailByVideoId") {
|
||||
// 获取 vId 参数
|
||||
val vIdParam = call.parameters["vId"]
|
||||
val vId = vIdParam?.toIntOrNull() // 将 vId 转换为 Int,确保安全
|
||||
if (vId == null) {
|
||||
call.respond(BaseResponse(status = true, message = "Invalid vId", data = null))
|
||||
return@get
|
||||
}
|
||||
val result = repository.getAnalyticsDetailByVideoId(vId)
|
||||
if (result != null) {
|
||||
call.respond(BaseResponse(data = result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun broadcastMessage(message: String) { // 封装的广播消息方法
|
||||
println("发送消息:$message")
|
||||
clients.forEach { client ->
|
||||
try {
|
||||
client.send(message) // 发送消息到每个客户端
|
||||
} catch (e: Exception) {
|
||||
println("发送消息给客户端时出错,移除客户端: ${e.message}")
|
||||
clients.remove(client) // 移除已断开的客户端
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,14 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.format
|
||||
import kotlinx.datetime.toJavaLocalDateTime
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.experimental.newSuspendedTransaction
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
|
||||
|
||||
fun getUserIdByToken(call: ApplicationCall) : Int?{
|
||||
// 通过token获取user_id
|
||||
return call.principal<JWTPrincipal>()?.payload?.getClaim("user_id")?.asInt()
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package ink.snowflake.server.utils.database
|
||||
|
||||
import ink.snowflake.server.model.database.AIProfile
|
||||
import ink.snowflake.server.model.database.AIProfilesTable
|
||||
import ink.snowflake.server.model.database.toAIProfile
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
object AIDao {
|
||||
/**
|
||||
* 获取所有AI
|
||||
*/
|
||||
fun getAllAIProfiles(): List<AIProfile> {
|
||||
return transaction {
|
||||
AIProfilesTable.selectAll().map { it.toAIProfile() }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user