# Project export: ClipGoal AI

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: An innovative AI app aimed at helping sports hobbyists autonomously film and clip in-game highlights.
- Devpost: https://devpost.com/software/clipgoal-ai
- GitHub: https://github.com/Yaowwwwww/ClipGoal-AI.git
- Video: https://www.youtube.com/embed/BgYwich0qW8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 0 GitHub contributor(s) — 

## Devpost submission (written by the team)

### Inspiration

Ever since 2nd grade, I’ve been a die-hard soccer fan. The first time I tried to edit and share my own match highlights, I was defeated by the frame-by-frame hunting → timeline alignment → export grind⁠—my excitement fizzled out, and I lost a valuable chance to review my play. So I teamed up with equally sports-obsessed Hackathon mates, fused my CS background with our love for other sports, and built ClipGoal AI: a one-tap highlight generator that will soon evolve into stats + motion-correction for any ball sport (soccer, basketball, volleyball, …)―putting AI in service of passion.

### Challenges we ran into

After nailing the welcome & home pages, the camera system stalled: reliably spotting a scoring zone across all sports proved tricky. Relying solely on generic algorithms was overkill, so we simplified by classifying sports into “Ground-Sport” vs “Goal-Oriented Sport” buckets, cutting both workflow and algorithmic complexity. Accomplishments we’re proud of Under brutal time pressure, we shipped a functional MVP that already meets the highlight-editing and training-video needs of everyday athletes.

### What we learned

The new React Native + Expo build pipeline & live debugging (npx expo start) How to write custom Vision-Camera Frame Processor plugins First exposure to event-driven design and bringing Expo Camera into production

### What's next

Full motion-correction mode – AI-powered coaching for amateurs Richer data dashboards – player stats & in-game performance visualisation More sports support – extend to additional ball sports with custom event rules

## README (from the GitHub repository)

# ClipGoal-AI Frontend

ClipGoal-AI 前端移动应用 - React Native + Expo实现的实时足球检测和录制应用

## 技术栈
- **React Native** (0.79.5) - 跨平台移动开发
- **Expo** (53.0.20) - 开发和部署平台  
- **TypeScript** - 类型安全
- **React Navigation** - 导航组件
- **Expo Camera** - 相机功能
- **React Native SVG** - 图形绘制

## 功能特性
- 📱 跨平台移动应用 (iOS/Android)
- 📹 实时相机预览和录制
- ⚽ 实时足球检测可视化
- 🥅 手动球门区域标注
- 🔴 大型录制按钮设计
- 🌐 WebSocket实时通信
- 📊 检测状态可视化

## 快速启动

### 环境要求
- Node.js 16+
- npm 或 yarn
- Expo CLI
- iOS模拟器 或 Android模拟器/设备

### 安装依赖
```bash
npm install
```

### 启动开发服务
```bash
npm start
```

### 运行到特定平台
```bash
npm run ios      # iOS模拟器
npm run android  # Android模拟器/设备
npm run web      # Web浏览器
```

## 配置说明

### 网络配置
在 `screens/RecordScreen.tsx` 中配置后端API地址：

```typescript
const getApiUrl = () => {
  if (Platform.OS === 'ios') {
    return 'http://YOUR_IP:8000'; // 替换为你的IP
  } else if (Platform.OS === 'android') {
    return 'http://10.0.2.2:8000'; // Android模拟器
  }
};
```

### 相机权限
应用会自动请求相机权限，请确保授权以使用检测功能。

## 主要界面

### 欢迎页 (WelcomeScreen)
- 应用介绍和导航入口

### 主页 (HomeScreen) 
- 运动项目选择界面

### 录制页 (RecordScreen)
- **实时相机预览**
- **AI检测结果可视化** (绿色框标记足球)
- **球门手动标注** (蓝色区域)
- **录制控制** (大红色圆形按钮)
- **检测状态指示器**

### 库页 (LibraryScreen)
- 录制的视频片段管理

## 核心功能

### 实时检测
- 每秒向后端发送帧进行AI分析
- 实时显示检测结果和边界框
- 置信度和坐标信息展示

### 球门标注
- 点击"标注球门"进入标注模式
- 按顺时针顺序点击4个角点
- 自动生成球门检测区域

### 录制功能
- 大红色圆形录制按钮
- 支持开始/停止录制
- 录制状态实时反馈
- 自动碰撞触发录制

### 碰撞检测
- 足球与球门区域重叠检测
- 自动触发录制功能
- 视觉提示和状态更新

## 文件结构
```
├── screens/           # 主要界面
│   ├── RecordScreen.tsx    # 核心录制界面
│   ├── HomeScreen.tsx      # 主页
│   ├── WelcomeScreen.tsx   # 欢迎页
│   └── LibraryScreen.tsx   # 视频库
├── navigation/        # 导航配置
│   └── TabNavigator.tsx    # 底部标签导航
├── assets/           # 静态资源
└── App.tsx          # 应用入口
```

## 开发说明

### 调试
- 使用Expo开发工具调试
- 摇动设备打开开发菜单
- 支持热重载和实时编辑

### 构建发布
```bash
# 创建构建
expo build:ios
expo build:android

# 或使用EAS Build
eas build --platform ios
eas build --platform android
```

## 网络配置指南

### iOS真机测试
需要配置本地IP地址，确保设备和开发机在同一WiFi网络

### Android模拟器
使用 `10.0.2.2` 作为host地址访问开发机

### 网络问题排查
1. 确保后端服务运行在 `0.0.0.0:8000`
2. 检查防火墙设置
3. 使用 `network_test.html` 测试连接

## 与后端集成
后端项目地址：https://github.com/Yaowwwwww/clipgoal-ai-backend

## 许可证
MIT License


## Detected evidence (automated analysis)

Indexed codebase: 33 recognized source files, 215 KB.
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- Python (language) — detected in the code
- PyTorch (technology) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- Supabase (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (47 of 47)

```
.DS_Store
.expo/README.md
.expo/settings.json
.gitignore
.vscode/settings.json
ai_model/requirements.txt
ai_model/soccer_detector.py
app.json
App.tsx
backend/app.py
backend/requirements.txt
backend/yolo11n.pt
backend/yolo11s.pt
CLAUDE.md
debug_collision.py
eas.json
index.ts
navigation/TabNavigator.tsx
network_test.html
package.json
README.md
screens/HomeScreen.tsx
screens/LibraryScreen.tsx
screens/RecordScreen.tsx
screens/WelcomeScreen.tsx
start_backend.py
test_files/check_ball_confidence.py
test_files/run_your_code.py
test_files/test_adjusted_detection.py
test_files/test_collision_fix.py
test_files/test_detection.py
test_files/test_disabled_features.py
test_files/test_enhanced_system.py
test_files/test_final_system.py
test_files/test_frame_by_frame.py
test_files/test_real_detection.py
test_files/test_upgraded_project.py
test_files/test_video_simple.py
test_files/test_video.py
test_files/test_websocket_client.py
test_files/test_websocket_realistic.py
test_files/test_yolo11s.py
test_files/test_your_method.py
test_websocket.html
tsconfig.json
yolo11n.pt
yolo11s.pt
```

### Dependencies

- ai_model/requirements.txt: albumentations@>=1.3.0, matplotlib@>=3.7.0, numpy@>=1.24.0, opencv-python@>=4.8.0, pandas@>=2.0.0, Pillow@>=10.0.0, PyYAML@>=6.0, roboflow@>=1.0.0, seaborn@>=0.12.0, tensorboard@>=2.13.0, torch@>=2.0.0, torchvision@>=0.15.0, tqdm@>=4.65.0, ultralytics@>=8.0.0, wandb@>=0.15.0
- backend/requirements.txt: fastapi@>=0.104.0, numpy@>=1.24.0, opencv-python@>=4.8.0, Pillow@>=10.0.0, python-multipart@>=0.0.6, torch@>=2.0.0, torchvision@>=0.15.0, ultralytics@>=8.0.0, uvicorn@>=0.24.0, websockets@>=12.0
- package.json: @babel/core@^7.25.2, @expo/metro-runtime@~5.0.4, @react-navigation/bottom-tabs@^7.3.13, @react-navigation/native@^7.1.9, @react-navigation/native-stack@^7.3.13, @types/react@~19.0.10, expo@53.0.20, expo-camera@~16.1.11, expo-status-bar@~2.2.3, react@19.0.0, react-dom@19.0.0, react-native@0.79.5, react-native-gesture-handler@~2.24.0, react-native-reanimated@~3.17.4, react-native-safe-area-context@5.4.0, react-native-screens@~4.11.1, react-native-svg@15.11.2, react-native-vector-icons@^10.2.0, react-native-web@^0.20.0, typescript@~5.8.3

### Recent commits (newest first)

- Merge with existing repository and resolve conflicts
- Initial commit: Frontend separation from ClipGoal-AI
- 更新球门标注UI配色方案为蓝色主题
- save1
- save
- 存档
- 存档

## Key source files (fetched from GitHub, selected and truncated for size)

### CLAUDE.md

```markdown
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

ClipGoal-AI is a React Native mobile app with Python backend that uses YOLOv11 AI to detect soccer balls and goals in real-time video, automatically generating highlight clips when ball-goal collisions are detected. The app provides real-time visual feedback with bounding boxes around detected objects.

## Architecture

### Frontend (React Native + Expo)
- **Main App**: `frontend/App.tsx` - Root component with navigation setup
- **Navigation**: `frontend/navigation/TabNavigator.tsx` - Bottom tab navigation
- **Core Screen**: `frontend/screens/RecordScreen.tsx` - Camera interface with real-time AI detection visualization
- **API Integration**: WebSocket connection to backend for real-time detection streaming
- **Visualization**: SVG overlays for real-time bounding boxes and detection feedback

### Backend (FastAPI + WebSocket)
- **Main Server**: `backend/app.py` - FastAPI server with WebSocket support for real-time frame processing
- **Endpoints**: `/health`, `/detect` (single frame), `/clips` (saved clips), WebSocket `/ws` (real-time stream)
- **Frame Management**: Global frame buffer for 10-second clip extraction

### AI Detection System
- **Core Engine**: `ai_model/soccer_detector.py` - SoccerDetector class with multi-modal detection
- **Detection Methods**: 
  - YOLOv11 for sports ball detection (COCO class ID=32)
  - Color-based soccer ball detection (white/black patterns)
  - Line-based goal detection using Hough transforms and edge detection
- **Collision Detection**: Distance-based algorithms for ball-goal collision detection
- **Clip Generation**: Automatic 10-second highlight extraction when collisions detected

## Commands

### Backend Development
```bash
# Start backend server (auto-installs dependencies and downloads YOLO model)
python start_backend.py

# Manual backend startup
cd backend && uvicorn app:app --host 0.0.0.0 --port 8000 --reload

# Install backend dependencies
pip install -r backend/requirements.txt
pip install -r ai_model/requirements.txt
```

### Frontend Development
```bash
# Install dependencies
cd frontend && npm install

# Start development server
npm start

# Run on specific platforms
npm run android
npm run ios
npm run web
```

### Testing
```bash
# Test AI detection functionality
python test_detection.py

# Test WebSocket connection
# Open test_websocket.html in browser

# Test network connectivity
# Open network_test.html in browser
```

## Network Configuration

The app uses platform-specific API endpoints:
- **iOS**: `192.168.0.103:8000` (local IP required for device testing)
- **Android Emulator**: `10.0.2.2:8000` (emulator bridge IP)
- **Development**: Local IP address must be configured in RecordScreen.tsx

## Key Components and Interactions

### Real-time Detection Flow
1. **Camera Feed**: RecordScreen captures frames at ~500ms intervals
2. **Frame Processing**: Frames 
[truncated — 1701 more characters]
```

### package.json

```
{
  "name": "frontend",
  "version": "1.0.0",
  "main": "index.ts",
  "scripts": {
    "start": "expo start",
    "android": "expo run:android",
    "ios": "expo run:ios",
    "web": "expo start --web"
  },
  "dependencies": {
    "@expo/metro-runtime": "~5.0.4",
    "@react-navigation/bottom-tabs": "^7.3.13",
    "@react-navigation/native": "^7.1.9",
    "@react-navigation/native-stack": "^7.3.13",
    "expo": "53.0.20",
    "expo-camera": "~16.1.11",
    "expo-status-bar": "~2.2.3",
    "react": "19.0.0",
    "react-dom": "19.0.0",
    "react-native": "0.79.5",
    "react-native-gesture-handler": "~2.24.0",
    "react-native-reanimated": "~3.17.4",
    "react-native-safe-area-context": "5.4.0",
    "react-native-screens": "~4.11.1",
    "react-native-svg": "15.11.2",
    "react-native-vector-icons": "^10.2.0",
    "react-native-web": "^0.20.0"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@types/react": "~19.0.10",
    "typescript": "~5.8.3"
  },
  "private": true
}

```

### backend/requirements.txt

```
fastapi>=0.104.0
uvicorn>=0.24.0
python-multipart>=0.0.6
websockets>=12.0
opencv-python>=4.8.0
numpy>=1.24.0
ultralytics>=8.0.0
torch>=2.0.0
torchvision>=0.15.0
Pillow>=10.0.0
```

### ai_model/requirements.txt

```
ultralytics>=8.0.0
torch>=2.0.0
torchvision>=0.15.0
opencv-python>=4.8.0
numpy>=1.24.0
Pillow>=10.0.0
matplotlib>=3.7.0
seaborn>=0.12.0
pandas>=2.0.0
tqdm>=4.65.0
PyYAML>=6.0
tensorboard>=2.13.0
albumentations>=1.3.0
roboflow>=1.0.0
wandb>=0.15.0
```

### index.ts

```typescript
import { registerRootComponent } from 'expo';

import App from './App';

// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);

```

### App.tsx

```typescript
// App.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

/* --------- 你的页面 / 导航 --------- */
import WelcomeScreen  from './screens/WelcomeScreen';   // << 确认路径大小写！
import TabNavigator   from './navigation/TabNavigator'; // << 你的底部 Tab

/* --------- 类型定义（可选但推荐） --------- */
export type RootStackParamList = {
  /** 首屏欢迎页（无参数） */
  Welcome: undefined;
  /** 主应用 — 承载底部 Tab 的那一层（同样无参数） */
  Main: undefined;

  // ➜ 如果以后还想加“选运动”等向导页，直接在这里追加：
  // SelectSport: undefined;
};

/* --------- 创建原生栈 --------- */
const Stack = createNativeStackNavigator<RootStackParamList>();

/* --------- 根组件 --------- */
export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator
        initialRouteName="Welcome"
        screenOptions={{ headerShown: false }}  // 全局隐藏原生标题栏
      >
        <Stack.Screen name="Welcome" component={WelcomeScreen} />
        <Stack.Screen name="Main"    component={TabNavigator} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

```

### backend/app.py

```python
"""
ClipGoal-AI 后端服务
提供实时足球和球门检测API
"""

from fastapi import FastAPI, File, UploadFile, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
import cv2
import numpy as np
import base64
import json
import asyncio
import time
import sys
import os

# 自定义JSON编码器，处理numpy数据类型
class NumpyEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (np.integer, np.floating)):
            return obj.item()
        elif isinstance(obj, np.ndarray):
            return obj.tolist()
        elif hasattr(obj, '__int__'):
            return int(obj)
        elif hasattr(obj, '__float__'):
            return float(obj)
        return super().default(obj)

# 添加AI模型路径
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'ai_model'))

from soccer_detector import SoccerDetector

app = FastAPI(title="ClipGoal-AI Detection API", version="1.0.0")

# 配置CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 延迟初始化检测器
detector = None

def get_detector():
    global detector
    if detector is None:
        print("正在初始化YOLO11s足球检测器...")
        detector = SoccerDetector(model_path='yolo11s.pt')
        print("✅ YOLO11s足球检测器初始化完成")
    return detector

# 存储连接的WebSocket客户端
active_connections = []
ball_history = []
frame_buffer = []  # 10秒帧缓冲区
saved_clips = []   # 保存的精彩片段


class ConnectionManager:
    def __init__(self):
        self.active_connections: list[WebSocket] = []

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.append(websocket)

    def disconnect(self, websocket: WebSocket):
        self.active_connections.remove(websocket)

    async def broadcast(self, data: dict):
        for connection in self.active_connections:
            try:
                await connection.send_text(json.dumps(data))
            except:
                # 连接已断开，移除它
                self.active_connections.remove(connection)


manager = ConnectionManager()


def decode_base64_image(base64_string: str) -> np.ndarray:
    """
    解码base64图像
    """
    try:
        # 移除数据URL前缀
        if 'base64,' in base64_string:
            base64_string = base64_string.split('base64,')[1]
        
        # 解码
        image_data = base64.b64decode(base64_string)
        nparr = np.frombuffer(image_data, np.uint8)
        image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
        
        return image
    except Exception as e:
        print(f"解码图像失败: {e}")
        return None


def encode_image_to_base64(image: np.ndarray) -> str:
    """
    编码图像为base64
    """
    try:
        _, buffer = cv2.imencode('.jpg', image)
        image_base64 = base64.b64encode(buffer).decode('utf-8')
        return f"data:image/jpeg;base64,{image_base64}"
    except Exception as e:
        print(f"编码图像失败: {e}")
        return ""


@app.get("/")
async def root():
    return {"message": "ClipGoal-AI Detection API"}


@app.post("/detect")
async def detect_image(file: UploadFile = File(...)):
    """
    检测上传的图像
    """
    try:
        # 读取图像
        contents = await file.read()
        nparr = np.frombuffer(contents, np.uint8)
        frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
        
        if frame is None:
            return {"error": "无法解码图像"}
        
        # 执行检测
        global ball_history, frame_buffer, saved_clips
        current_detector = get_detector()
        result = current_detector.process_frame(frame, ball_history, frame_buffer)
        ball_history = result['ball_history']
        frame_buffer = result['frame_buffer']
        
        # 如果检测到碰撞，保存片段信息
        if result['clip_info']:
            saved_clips.append(result['clip_info'])
            print(f"🎥 保存精彩片段: {result['clip_info']['collision_type']}, 帧数: {result['clip_info']['frame_count']}")
            
            # 限制保存的片段数量
            if len(saved_clips) > 50:
                saved_clips.pop(0)
        
        # 绘制检测结果
        output_frame = current_detector.draw_detections(frame, result)
        output_base64 = encode_image_to_base64(output_frame)
        
        # 准备响应数据
        response_data = {
            "success": True,
            "detections": {
                "soccer_balls": result['detections']['soccer_balls'],
                "goal_areas": result['detections']['goal_areas']
            },
            "collision_info": result['collision_info'],
            "is_goal_moment": result['is_goal_moment'],
            "trajectory": result['trajectory'],
            "clip_info": result['clip_info'],
            "processed_image": output_base64,
            "timestamp": result['timestamp']
        }
        
        return response_data
        
    except Exception as e:
        return {"error": f"检测失败: {str(e)}"}


@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    """
    实时检测WebSocket端点 - YOLO11s逐帧处理
    """
    await manager.connect(websocket)
    global ball_history
    frame_count = 0
    
    try:
        while True:
            # 接收来自客户端的数据
            data = await websocket.receive_text()
            frame_data = json.loads(data)
            
            # 解码图像
            frame = decode_base64_image(frame_data['image'])
            
            if frame is not None:
                frame_count += 1
                start_time = time.time()
                
                print(f"📷 帧{frame_count}: 尺寸{frame.shape[1]}x{frame.shape[0]}")
                
                # 执行YOLO11s检测
                global frame_buffer, saved_clips
                current_detector = get_detector()
                result = current_detector.process_frame(frame, ball_history, frame_buffer)
                
                processing_time = (time.time() - start_time) * 1000
                ball_count = len(result['detections']['soccer_balls'])
                
                print(f"✅ 帧{frame_count}: 检测到{ball_count}个足球, 耗时{processing_time:.1f}ms")
[truncated — 6523 more characters]
```

### start_backend.py

```python
#!/usr/bin/env python3
"""
启动ClipGoal-AI后端服务
"""

import subprocess
import sys
import os
import time

def check_requirements():
    """检查依赖是否已安装"""
    try:
        import fastapi
        import uvicorn
        import cv2
        import numpy as np
        import ultralytics
        import torch
        print("✅ 所有依赖已安装")
        return True
    except ImportError as e:
        print(f"❌ 缺少依赖: {e}")
        return False

def install_requirements():
    """安装依赖"""
    requirements_files = [
        "backend/requirements.txt",
        "ai_model/requirements.txt"
    ]
    
    for req_file in requirements_files:
        if os.path.exists(req_file):
            print(f"正在安装 {req_file} 中的依赖...")
            subprocess.run([sys.executable, "-m", "pip", "install", "-r", req_file])

def download_yolo_model():
    """下载YOLOv11模型"""
    try:
        from ultralytics import YOLO
        print("正在下载YOLOv11模型...")
        model = YOLO('yolo11n.pt')  # 下载nano版本
        print("✅ YOLOv11模型下载完成")
        return True
    except Exception as e:
        print(f"❌ 模型下载失败: {e}")
        return False

def start_server():
    """启动FastAPI服务器"""
    try:
        os.chdir("backend")
        print("🚀 启动ClipGoal-AI后端服务...")
        print("服务地址: http://localhost:8000")
        print("API文档: http://localhost:8000/docs")
        print("按 Ctrl+C 停止服务")
        
        subprocess.run([
            sys.executable, "-m", "uvicorn", 
            "app:app", 
            "--host", "0.0.0.0", 
            "--port", "8000", 
            "--reload"
        ])
    except KeyboardInterrupt:
        print("\n🛑 服务已停止")
    except Exception as e:
        print(f"❌ 启动服务失败: {e}")

def main():
    print("🎯 ClipGoal-AI 后端服务启动脚本")
    print("=" * 40)
    
    # 检查并安装依赖
    if not check_requirements():
        print("正在安装依赖...")
        install_requirements()
        time.sleep(2)
    
    # 下载YOLO模型
    if not download_yolo_model():
        print("⚠️ 模型下载失败，但仍可继续启动服务")
    
    # 启动服务
    start_server()

if __name__ == "__main__":
    main()
```

### debug_collision.py

```python
#!/usr/bin/env python3
"""
调试碰撞检测问题
"""
import sys
import os
sys.path.append('ai_model')

from soccer_detector import SoccerDetector
import cv2
import numpy as np

def debug_collision_detection():
    """调试碰撞检测"""
    print("🔍 调试碰撞检测问题")
    print("=" * 50)
    
    detector = SoccerDetector()
    
    # 创建测试图像：球在球门内
    frame = np.ones((480, 640, 3), dtype=np.uint8) * 50
    frame[:, :, 1] = 120  # 绿色背景
    
    # 添加一个球门
    cv2.rectangle(frame, (200, 150), (400, 250), (255, 255, 255), 4)
    # 在球门内添加一个白色球
    cv2.circle(frame, (300, 200), 25, (255, 255, 255), -1)
    cv2.circle(frame, (300, 200), 25, (0, 0, 0), 2)
    
    # 获取检测结果
    detection_result = detector.detect_objects(frame)
    
    print(f"🏀 原始球类检测数量: {len(detection_result['soccer_balls'])}")
    print("🏀 球类详细信息:")
    for i, ball in enumerate(detection_result['soccer_balls']):
        print(f"   球{i+1}: 置信度={ball['confidence']:.3f}, 方法={ball.get('detection_method', 'unknown')}, 中心={ball['center']}")
    
    print(f"\n🥅 原始球门检测数量: {len(detection_result['goal_areas'])}")
    print("🥅 球门详细信息:")
    for i, goal in enumerate(detection_result['goal_areas']):
        print(f"   门{i+1}: 置信度={goal['confidence']:.3f}, 方法={goal.get('detection_method', 'unknown')}, bbox={goal['bbox']}")
    
    # 手动测试碰撞检测逻辑
    print(f"\n🔧 手动测试碰撞检测:")
    
    # 过滤后的球和门
    valid_balls = [ball for ball in detection_result['soccer_balls'] if ball['confidence'] > 0.4]
    valid_goals = [goal for goal in detection_result['goal_areas'] if goal['confidence'] > 0.3]
    
    print(f"   📏 置信度过滤后 - 球: {len(valid_balls)}, 门: {len(valid_goals)}")
    
    if valid_balls and valid_goals:
        ball = valid_balls[0]
        goal = valid_goals[0]
        
        ball_x, ball_y = ball['center']
        ball_bbox = ball['bbox']
        goal_bbox = goal['bbox']
        
        print(f"   🏀 球中心: ({ball_x:.1f}, {ball_y:.1f})")
        print(f"   🏀 球边界框: {ball_bbox}")
        print(f"   🥅 门边界框: {goal_bbox}")
        
        # 检查球是否在门内
        ball_radius = max(ball_bbox[2] - ball_bbox[0], ball_bbox[3] - ball_bbox[1]) / 2
        ball_left = ball_x - ball_radius
        ball_right = ball_x + ball_radius
        ball_top = ball_y - ball_radius
        ball_bottom = ball_y + ball_radius
        
        print(f"   🏀 球完整边界: 左={ball_left:.1f}, 右={ball_right:.1f}, 上={ball_top:.1f}, 下={ball_bottom:.1f}")
        
        in_goal = (goal_bbox[0] < ball_left and ball_right < goal_bbox[2] and 
                  goal_bbox[1] < ball_top and ball_bottom < goal_bbox[3])
        
        print(f"   ⚽ 球是否完全在门内: {in_goal}")
        
        if not in_goal:
            # 检查简单的中心点是否在门内
            simple_in_goal = (goal_bbox[0] <= ball_x <= goal_bbox[2] and 
                            goal_bbox[1] <= ball_y <= goal_bbox[3])
            print(f"   ⚽ 球中心是否在门内: {simple_in_goal}")
    
    # 运行完整的碰撞检测
    collision = detector.check_ball_goal_collision(detection_result['soccer_balls'], detection_result['goal_areas'])
    print(f"\n💥 最终碰撞检测结果: {collision['has_collision']}")
    print(f"💥 碰撞类型: {collision.get('collision_type', 'None')}")

if __name__ == "__main__":
    debug_collision_detection()
```

### test_websocket.html

```html
<!DOCTYPE html>
<html>
<head>
    <title>ClipGoal-AI WebSocket 测试</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        #status { padding: 10px; margin: 10px 0; border-radius: 5px; }
        .connected { background-color: #d4edda; color: #155724; }
        .disconnected { background-color: #f8d7da; color: #721c24; }
        .error { background-color: #fff3cd; color: #856404; }
        button { padding: 10px 15px; margin: 5px; cursor: pointer; }
        #messages { border: 1px solid #ccc; height: 300px; overflow-y: auto; padding: 10px; }
        input[type="file"] { margin: 10px 0; }
    </style>
</head>
<body>
    <h1>ClipGoal-AI WebSocket 连接测试</h1>
    
    <div id="status" class="disconnected">
        状态: 未连接
    </div>
    
    <button onclick="connectWebSocket()">连接 WebSocket</button>
    <button onclick="disconnectWebSocket()">断开连接</button>
    <button onclick="testHealthCheck()">测试健康检查</button>
    
    <h3>测试图片上传检测</h3>
    <input type="file" id="imageInput" accept="image/*" onchange="handleImageUpload()">
    <button onclick="sendTestFrame()">发送测试帧</button>
    
    <h3>消息日志</h3>
    <div id="messages"></div>

    <script>
        let ws = null;
        let isConnected = false;

        function updateStatus(message, type = 'disconnected') {
            const statusDiv = document.getElementById('status');
            statusDiv.textContent = `状态: ${message}`;
            statusDiv.className = type;
        }

        function addMessage(message, type = 'info') {
            const messagesDiv = document.getElementById('messages');
            const timestamp = new Date().toLocaleTimeString();
            const messageElement = document.createElement('div');
            messageElement.style.marginBottom = '5px';
            messageElement.style.padding = '5px';
            messageElement.style.borderLeft = `3px solid ${type === 'error' ? 'red' : type === 'success' ? 'green' : 'blue'}`;
            messageElement.innerHTML = `<strong>[${timestamp}]</strong> ${message}`;
            messagesDiv.appendChild(messageElement);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        }

        function connectWebSocket() {
            try {
                addMessage('正在连接 WebSocket...');
                ws = new WebSocket('ws://localhost:8000/ws');
                
                ws.onopen = function(event) {
                    isConnected = true;
                    updateStatus('WebSocket 已连接', 'connected');
                    addMessage('✅ WebSocket 连接成功', 'success');
                };
                
                ws.onmessage = function(event) {
                    try {
                        const data = JSON.parse(event.data);
                        addMessage(`收到检测结果: ${JSON.stringify(data, null, 2)}`, 'success');
                        
                        if (data.success) {
                            const balls = data.detections?.soccer_balls?.length || 0;
                            const goal = data.detections?.goal_area ? '已检测' : '未检测';
                            const isGoal = data.is_goal_moment ? '⚽ 进球!' : '';
                            addMessage(`足球: ${balls} | 球门: ${goal} ${isGoal}`, 'success');
                        }
                    } catch (error) {
                        addMessage(`解析消息失败: ${error.message}`, 'error');
                    }
                };
                
                ws.onerror = function(error) {
                    addMessage(`WebSocket 错误: ${error}`, 'error');
                    updateStatus('连接错误', 'error');
                };
                
                ws.onclose = function(event) {
                    isConnected = false;
                    updateStatus('连接已关闭', 'disconnected');
                    addMessage(`连接已关闭，代码: ${event.code}`, 'error');
                };
            } catch (error) {
                addMessage(`连接失败: ${error.message}`, 'error');
            }
        }

        function disconnectWebSocket() {
            if (ws) {
                ws.close();
                ws = null;
            }
            isConnected = false;
            updateStatus('已断开连接', 'disconnected');
            addMessage('已主动断开连接');
        }

        async function testHealthCheck() {
            try {
                addMessage('正在测试健康检查...');
                const response = await fetch('http://localhost:8000/health');
                const data = await response.json();
                addMessage(`健康检查结果: ${JSON.stringify(data, null, 2)}`, 'success');
            } catch (error) {
                addMessage(`健康检查失败: ${error.message}`, 'error');
            }
        }

        function handleImageUpload() {
            const input = document.getElementById('imageInput');
            const file = input.files[0];
            if (file) {
                addMessage(`已选择图片: ${file.name} (${(file.size / 1024).toFixed(2)} KB)`);
            }
        }

        function sendTestFrame() {
            if (!isConnected) {
                addMessage('请先连接 WebSocket', 'error');
                return;
            }

            const input = document.getElementById('imageInput');
            const file = input.files[0];
            
            if (!file) {
                // 发送一个测试用的小图片数据
                const canvas = document.createElement('canvas');
                canvas.width = 100;
                canvas.height = 100;
                const ctx = canvas.getContext('2d');
                
                // 绘制一个简单的测试图案
                ctx.fillStyle = '#00FF00';
                ctx.fillRect(0, 0, 100, 100);
                ctx.fillStyle = '#FF0000';
                ctx.fillRect(25, 25, 50, 50);
                
                const base64Data = canvas.toDataURL('image/jpeg', 0.5);
                
                const frameData = {
                    image: base64Data
                };
                
                addMessage('发送测试帧数据...');
              
[truncated — 663 more characters]
```

[23 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]