diff --git a/scripts/ddl.sql b/scripts/ddl.sql new file mode 100644 index 0000000..4f98879 --- /dev/null +++ b/scripts/ddl.sql @@ -0,0 +1,131 @@ +-- ============================================================ +-- Sentinel Home AI - 数据库 DDL +-- 版本: v1.0 +-- 日期: 2026-08-19 +-- 数据库: MariaDB (MySQL 兼容) +-- ============================================================ + +-- 创建数据库(如不存在) +CREATE DATABASE IF NOT EXISTS sentinel_home_ai + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +USE sentinel_home_ai; + +-- ============================================================ +-- 1. 任务表 process_tasks +-- ============================================================ +CREATE TABLE IF NOT EXISTS process_tasks ( + task_id INT AUTO_INCREMENT PRIMARY KEY, + video_path VARCHAR(500) NOT NULL COMMENT '视频在 NAS 上的绝对路径', + video_url VARCHAR(500) NOT NULL COMMENT 'Video-Server 提供的下载 URL', + status ENUM('PENDING','PROCESSING','SUCCESS','FAILED') DEFAULT 'PENDING', + retry_count INT DEFAULT 0, + max_retries INT DEFAULT 3, + next_retry_at DATETIME NULL COMMENT '下次重试时间(退避策略)', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + error_message TEXT NULL, + failure_stage ENUM('download','extract','vlm_visual','vlm_fusion','callback') NULL COMMENT '失败阶段', + heartbeat_at DATETIME NULL COMMENT '心跳时间戳(首期预留,不写入)', + INDEX idx_status (status), + INDEX idx_created (created_at), + INDEX idx_next_retry (next_retry_at) +) ENGINE=InnoDB COMMENT='视频分析任务表'; + +-- ============================================================ +-- 2. 事件表 monitor_events +-- ============================================================ +CREATE TABLE IF NOT EXISTS monitor_events ( + event_id INT AUTO_INCREMENT PRIMARY KEY, + task_id INT NOT NULL, + event_start_time DATETIME NOT NULL COMMENT '事件开始时间', + event_end_time DATETIME NOT NULL COMMENT '事件结束时间', + camera_name VARCHAR(50) COMMENT '摄像头名称/位置', + global_summary TEXT COMMENT 'VLM 生成的整体摘要', + entities_json JSON NOT NULL COMMENT '实体列表 JSON 数组', + compute_provider JSON NOT NULL COMMENT '模型来源数组,如 ["ollama","gemini"]', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_event_start (event_start_time), + INDEX idx_compute_provider (compute_provider), + FOREIGN KEY (task_id) REFERENCES process_tasks(task_id) ON DELETE CASCADE +) ENGINE=InnoDB COMMENT='监控事件聚合表'; + +-- ============================================================ +-- 3. 每日摘要表 daily_summaries(预留) +-- ============================================================ +CREATE TABLE IF NOT EXISTS daily_summaries ( + id INT AUTO_INCREMENT PRIMARY KEY, + target_date DATE NOT NULL UNIQUE COMMENT '目标日期', + summary_text TEXT COMMENT '当日摘要文本', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB COMMENT='每日摘要表(首期预留)'; + +-- ============================================================ +-- 4. 事件明细表 event_details +-- 每关键帧一条,由 VLM 输出 frame_details 拆分落库 +-- ============================================================ +CREATE TABLE IF NOT EXISTS event_details ( + detail_id INT AUTO_INCREMENT PRIMARY KEY, + event_id INT NOT NULL COMMENT '关联 monitor_events.event_id', + task_id INT NOT NULL COMMENT '冗余,便于查询', + frame_index INT NOT NULL COMMENT '关键帧序号(1-8)', + frame_timestamp DATETIME NOT NULL COMMENT '该帧对应的绝对时间点', + camera_name VARCHAR(50) COMMENT '摄像头位置(如"客厅")', + person VARCHAR(50) NOT NULL COMMENT '成员名或"人物A/B/C"或"未知访客"', + action VARCHAR(200) NOT NULL COMMENT '动作描述(AI 自由生成,无枚举)', + clothing VARCHAR(100) COMMENT '衣着描述', + is_attention_event BOOLEAN DEFAULT FALSE COMMENT 'AI 自行判断是否为关注事件', + source_providers JSON NOT NULL COMMENT '识别到该明细的模型来源数组', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_event_id (event_id), + INDEX idx_frame_timestamp (frame_timestamp), + INDEX idx_person (person), + INDEX idx_queried_date (frame_timestamp), + FOREIGN KEY (event_id) REFERENCES monitor_events(event_id) ON DELETE CASCADE +) ENGINE=InnoDB COMMENT='事件明细表(每关键帧一条)'; + +-- ============================================================ +-- 5. 对话记录表 chat_history +-- ============================================================ +CREATE TABLE IF NOT EXISTS chat_history ( + chat_id INT AUTO_INCREMENT PRIMARY KEY, + user_question TEXT NOT NULL COMMENT '用户问题', + ai_answer TEXT NOT NULL COMMENT 'AI 回答', + context_summary TEXT COMMENT '问答时使用的上下文摘要', + queried_date DATE COMMENT '查询的日期范围', + queried_person VARCHAR(50) COMMENT '查询的人物', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_created (created_at), + INDEX idx_queried_date (queried_date), + INDEX idx_queried_person (queried_person) +) ENGINE=InnoDB COMMENT='用户与 AI 问答记录表'; + +-- ============================================================ +-- 6. 家庭成员表 family_members +-- 交互式命名:VLM 自动按特征提取"人物A/B/C",用户命名为真实名字 +-- ============================================================ +CREATE TABLE IF NOT EXISTS family_members ( + member_id INT AUTO_INCREMENT PRIMARY KEY, + abstract_label VARCHAR(20) NOT NULL UNIQUE COMMENT 'VLM 首次提取的抽象标识,如"人物A"', + real_name VARCHAR(50) COMMENT '用户命名的真实名字,如"汤圆";NULL 表示未命名', + feature_description TEXT COMMENT 'VLM 提取的特征描述', + first_seen_at DATETIME COMMENT '首次被提取的时间', + named_at DATETIME NULL COMMENT '用户命名的时间', + named_by VARCHAR(50) COMMENT '命名人', + is_active BOOLEAN DEFAULT TRUE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_real_name (real_name), + INDEX idx_abstract_label (abstract_label), + INDEX idx_is_active (is_active) +) ENGINE=InnoDB COMMENT='家庭成员表(交互式命名)'; + +-- ============================================================ +-- 验证 +-- ============================================================ +SELECT 'DDL 执行完成' AS message; +SELECT TABLE_NAME, TABLE_COMMENT +FROM information_schema.TABLES +WHERE TABLE_SCHEMA = 'sentinel_home_ai' +ORDER BY TABLE_NAME; diff --git a/scripts/init_db.py b/scripts/init_db.py new file mode 100644 index 0000000..671d40e --- /dev/null +++ b/scripts/init_db.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +""" +Sentinel Home AI - 数据库初始化脚本 +执行 DDL 创建所有表,并可插入测试数据。 +""" +import sys +import os +import argparse + +import mysql.connector + + +def get_connection(host='127.0.0.1', port=3306, user='root', password=''): + """获取 MariaDB 连接""" + return mysql.connector.connect( + host=host, port=port, user=user, password=password, + charset='utf8mb4', collation='utf8mb4_unicode_ci' + ) + + +def execute_ddl(conn): + """执行 DDL 脚本""" + ddl_path = os.path.join(os.path.dirname(__file__), 'ddl.sql') + with open(ddl_path, 'r', encoding='utf-8') as f: + ddl_sql = f.read() + + cursor = conn.cursor() + # 按分号分割执行(简单方案,适合无存储过程的 DDL) + statements = [s.strip() for s in ddl_sql.split(';') if s.strip() and not s.strip().startswith('--')] + for stmt in statements: + if stmt.upper().startswith('SELECT'): + cursor.execute(stmt) + for row in cursor.fetchall(): + print(f" {row[0]}: {row[1] if len(row) > 1 else ''}") + else: + cursor.execute(stmt) + print(f" OK: {stmt[:60]}...") + conn.commit() + cursor.close() + print("\n[OK] DDL 执行完成") + + +def insert_test_data(conn): + """插入测试数据""" + cursor = conn.cursor() + + # 插入测试任务 + cursor.execute(""" + INSERT INTO process_tasks (video_path, video_url, status) + VALUES (%s, %s, %s) + """, ('/volume1/surveillance/test.mp4', 'http://100.x.x.10:8000/media/test.mp4?token=xxx', 'SUCCESS')) + + task_id = cursor.lastrowid + + # 插入测试事件 + cursor.execute(""" + INSERT INTO monitor_events (task_id, event_start_time, event_end_time, camera_name, global_summary, entities_json, compute_provider) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """, ( + task_id, + '2026-08-19 14:00:00', + '2026-08-19 14:30:00', + '客厅', + '客厅有人员走动,汤圆在玩积木。', + '[{"person": "人物A", "action": "在地毯上玩积木", "clothing": "黄色T恤"}]', + '["ollama"]' + )) + event_id = cursor.lastrowid + + # 插入测试事件明细 + cursor.execute(""" + INSERT INTO event_details (event_id, task_id, frame_index, frame_timestamp, camera_name, person, action, clothing, is_attention_event, source_providers) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, ( + event_id, task_id, 1, '2026-08-19 14:03:00', '客厅', '人物A', '在地毯上玩积木', '黄色T恤', False, '["ollama"]' + )) + cursor.execute(""" + INSERT INTO event_details (event_id, task_id, frame_index, frame_timestamp, camera_name, person, action, clothing, is_attention_event, source_providers) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, ( + event_id, task_id, 2, '2026-08-19 14:15:00', '客厅', '人物A', '看绘本', '黄色T恤', False, '["ollama"]' + )) + cursor.execute(""" + INSERT INTO event_details (event_id, task_id, frame_index, frame_timestamp, camera_name, person, action, clothing, is_attention_event, source_providers) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, ( + event_id, task_id, 3, '2026-08-19 14:25:00', '客厅', '人物A', '跌倒', '黄色T恤', True, '["ollama"]' + )) + + # 插入测试成员 + cursor.execute(""" + INSERT IGNORE INTO family_members (abstract_label, real_name, feature_description, first_seen_at) + VALUES (%s, %s, %s, %s) + """, ('人物A', None, '短发、黄色T恤、男性', '2026-08-19 14:03:00')) + + conn.commit() + cursor.close() + print(f"[OK] 测试数据插入完成 (task_id={task_id}, event_id={event_id})") + + +def main(): + parser = argparse.ArgumentParser(description='Sentinel Home AI 数据库初始化') + parser.add_argument('--host', default='127.0.0.1', help='MariaDB 主机') + parser.add_argument('--port', type=int, default=3306, help='MariaDB 端口') + parser.add_argument('--user', default='root', help='MariaDB 用户名') + parser.add_argument('--password', default='', help='MariaDB 密码') + parser.add_argument('--test-data', action='store_true', help='插入测试数据') + args = parser.parse_args() + + print(f"连接 MariaDB {args.host}:{args.port} ...") + conn = get_connection(args.host, args.port, args.user, args.password) + + print("执行 DDL ...") + execute_ddl(conn) + + if args.test_data: + print("\n插入测试数据 ...") + insert_test_data(conn) + + conn.close() + print("\n完成!") + + +if __name__ == '__main__': + main()