v1.0.2发布,支持视频分析结果查看

This commit is contained in:
BBIT-Kai
2025-06-04 09:36:43 +08:00
parent a3f51dee7d
commit 89adaf02b9
62 changed files with 5975 additions and 4584 deletions
@@ -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() }
}
}
}
@@ -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 {
/**
@@ -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],
@@ -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()
@@ -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()
@@ -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") {
@@ -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(外键)
@@ -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)
@@ -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()
}
@@ -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") {
@@ -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())
}
}
@@ -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"
)
}
}
@@ -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 // 处理时间
)
)
@@ -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,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
}
}
@@ -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.*
@@ -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))
}
}
@@ -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))
@@ -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,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() }
}
}
}