docs: add speckit planning artifacts for 001-ai-service-requirements
Generated plan.md, research.md, data-model.md, contracts/api.md, quickstart.md, and CLAUDE.md agent context via /speckit-plan.
This commit is contained in:
333
specs/001-ai-service-requirements/contracts/api.md
Normal file
333
specs/001-ai-service-requirements/contracts/api.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# API Contract: AI 服务接口定义
|
||||
|
||||
**Branch**: `001-ai-service-requirements` | **Date**: 2026-04-10
|
||||
**Base URL**: `http://ai-service:8000`
|
||||
**API Prefix**: `/api/v1`
|
||||
**Swagger**: `/docs`(FastAPI 自动生成)
|
||||
|
||||
---
|
||||
|
||||
## 通用约定
|
||||
|
||||
### 请求格式
|
||||
- 所有请求体:`Content-Type: application/json`
|
||||
- 无认证机制(内网服务,仅 Java 后端调用)
|
||||
|
||||
### 响应格式
|
||||
- 成功:HTTP 2xx,JSON 响应体
|
||||
- 错误:HTTP 4xx/5xx,统一错误格式:
|
||||
```json
|
||||
{"code": "ERROR_CODE", "message": "具体描述"}
|
||||
```
|
||||
|
||||
### 错误码
|
||||
|
||||
| HTTP 状态码 | code | 触发条件 |
|
||||
|------------|------|---------|
|
||||
| 400 | UNSUPPORTED_FILE_TYPE | 文件格式不支持(如 .xlsx) |
|
||||
| 400 | VIDEO_TOO_LARGE | 视频文件超过大小上限 |
|
||||
| 502 | STORAGE_ERROR | RustFS 不可达或文件不存在 |
|
||||
| 502 | LLM_PARSE_ERROR | GLM 返回非合法 JSON |
|
||||
| 503 | LLM_CALL_ERROR | GLM API 限流 / 超时 |
|
||||
| 500 | INTERNAL_ERROR | 未捕获异常 |
|
||||
|
||||
---
|
||||
|
||||
## 端点一览
|
||||
|
||||
| 端点 | 方法 | 功能 | 响应码 |
|
||||
|------|------|------|--------|
|
||||
| `/health` | GET | 健康检查 | 200 |
|
||||
| `/api/v1/text/extract` | POST | 文档三元组提取 | 200 |
|
||||
| `/api/v1/image/extract` | POST | 图像四元组提取 | 200 |
|
||||
| `/api/v1/video/extract-frames` | POST | 视频帧提取(异步) | 202 |
|
||||
| `/api/v1/video/to-text` | POST | 视频转文本(异步) | 202 |
|
||||
| `/api/v1/qa/gen-text` | POST | 文本问答对生成 | 200 |
|
||||
| `/api/v1/qa/gen-image` | POST | 图像问答对生成 | 200 |
|
||||
| `/api/v1/finetune/start` | POST | 提交微调任务 | 200 |
|
||||
| `/api/v1/finetune/status/{jobId}` | GET | 查询微调状态 | 200 |
|
||||
|
||||
---
|
||||
|
||||
## 端点详情
|
||||
|
||||
### GET /health
|
||||
|
||||
健康检查端点,无需认证,无请求体。
|
||||
|
||||
**响应(200 OK)**:
|
||||
```json
|
||||
{"status": "ok"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/text/extract
|
||||
|
||||
从存储中指定路径的文档提取文本三元组。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"file_path": "text/202404/123.txt",
|
||||
"file_name": "设备规范.txt",
|
||||
"model": "glm-4-flash",
|
||||
"prompt_template": "..."
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| file_path | string | 是 | RustFS 中的文件路径 |
|
||||
| file_name | string | 是 | 带扩展名的文件名(用于判断格式) |
|
||||
| model | string | 否 | 模型名,默认使用 config 中的 default_text |
|
||||
| prompt_template | string | 否 | 自定义提示词,不传使用内置模板 |
|
||||
|
||||
**支持格式**: `.txt`, `.pdf`, `.docx`
|
||||
|
||||
**响应(200 OK)**:
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"subject": "变压器",
|
||||
"predicate": "额定电压",
|
||||
"object": "110kV",
|
||||
"source_snippet": "该变压器额定电压为110kV",
|
||||
"source_offset": {"start": 120, "end": 150}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/image/extract
|
||||
|
||||
从存储中指定路径的图片提取知识四元组,并自动裁剪 bbox 区域。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"file_path": "image/202404/456.jpg",
|
||||
"task_id": 789,
|
||||
"model": "glm-4v-flash",
|
||||
"prompt_template": "..."
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| file_path | string | 是 | RustFS 中的图片路径 |
|
||||
| task_id | int | 是 | 标注任务 ID(用于构造裁剪图存储路径) |
|
||||
| model | string | 否 | 默认使用 config 中的 default_vision |
|
||||
| prompt_template | string | 否 | 自定义提示词 |
|
||||
|
||||
**响应(200 OK)**:
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"subject": "电缆接头",
|
||||
"predicate": "位于",
|
||||
"object": "配电箱左侧",
|
||||
"qualifier": "2024年检修现场",
|
||||
"bbox": {"x": 10, "y": 20, "w": 100, "h": 80},
|
||||
"cropped_image_path": "crops/789/0.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/video/extract-frames
|
||||
|
||||
触发视频帧提取后台任务,立即返回。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"file_path": "video/202404/001.mp4",
|
||||
"source_id": 10,
|
||||
"job_id": 42,
|
||||
"mode": "interval",
|
||||
"frame_interval": 30
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| file_path | string | 是 | RustFS 中的视频路径 |
|
||||
| source_id | int | 是 | 原始资料 ID(用于构造帧存储路径) |
|
||||
| job_id | int | 是 | 由 Java 后端分配的任务 ID |
|
||||
| mode | string | 否 | `interval`(默认)或 `keyframe` |
|
||||
| frame_interval | int | 否 | interval 模式专用,按帧数步进,默认 30 |
|
||||
|
||||
**响应(202 Accepted)**:
|
||||
```json
|
||||
{"message": "任务已接受,后台处理中", "job_id": 42}
|
||||
```
|
||||
|
||||
**完成后回调 Java 后端**(POST `{BACKEND_CALLBACK_URL}`):
|
||||
```json
|
||||
{
|
||||
"job_id": 42,
|
||||
"status": "SUCCESS",
|
||||
"frames": [
|
||||
{"frame_index": 0, "time_sec": 0.0, "frame_path": "frames/10/0.jpg"}
|
||||
],
|
||||
"error_message": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/video/to-text
|
||||
|
||||
触发视频片段转文字后台任务,立即返回。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"file_path": "video/202404/001.mp4",
|
||||
"source_id": 10,
|
||||
"job_id": 43,
|
||||
"start_sec": 0,
|
||||
"end_sec": 120,
|
||||
"model": "glm-4v-flash",
|
||||
"prompt_template": "..."
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| file_path | string | 是 | RustFS 中的视频路径 |
|
||||
| source_id | int | 是 | 原始资料 ID |
|
||||
| job_id | int | 是 | 由 Java 后端分配的任务 ID |
|
||||
| start_sec | float | 是 | 分析起始时间(秒) |
|
||||
| end_sec | float | 是 | 分析结束时间(秒) |
|
||||
| model | string | 否 | 默认使用 config 中的 default_vision |
|
||||
| prompt_template | string | 否 | 自定义提示词 |
|
||||
|
||||
**响应(202 Accepted)**:
|
||||
```json
|
||||
{"message": "任务已接受,后台处理中", "job_id": 43}
|
||||
```
|
||||
|
||||
**完成后回调 Java 后端**(POST `{BACKEND_CALLBACK_URL}`):
|
||||
```json
|
||||
{
|
||||
"job_id": 43,
|
||||
"status": "SUCCESS",
|
||||
"output_path": "video-text/10/1712800000.txt",
|
||||
"error_message": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/qa/gen-text
|
||||
|
||||
基于文本三元组批量生成候选问答对。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"subject": "变压器",
|
||||
"predicate": "额定电压",
|
||||
"object": "110kV",
|
||||
"source_snippet": "该变压器额定电压为110kV"
|
||||
}
|
||||
],
|
||||
"model": "glm-4-flash",
|
||||
"prompt_template": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**响应(200 OK)**:
|
||||
```json
|
||||
{
|
||||
"pairs": [
|
||||
{"question": "变压器的额定电压是多少?", "answer": "该变压器额定电压为110kV。"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/qa/gen-image
|
||||
|
||||
基于图像四元组生成候选图文问答对。图片由 AI 服务从存储自动获取,调用方只需提供路径。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"subject": "电缆接头",
|
||||
"predicate": "位于",
|
||||
"object": "配电箱左侧",
|
||||
"qualifier": "2024年检修现场",
|
||||
"cropped_image_path": "crops/789/0.jpg"
|
||||
}
|
||||
],
|
||||
"model": "glm-4v-flash",
|
||||
"prompt_template": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**响应(200 OK)**:
|
||||
```json
|
||||
{
|
||||
"pairs": [
|
||||
{
|
||||
"question": "图中电缆接头位于何处?",
|
||||
"answer": "图中电缆接头位于配电箱左侧。",
|
||||
"image_path": "crops/789/0.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/finetune/start
|
||||
|
||||
向 ZhipuAI 提交微调任务。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"jsonl_url": "https://rustfs.example.com/finetune-export/export/xxx.jsonl",
|
||||
"base_model": "glm-4-flash",
|
||||
"hyperparams": {"learning_rate": 1e-4, "epochs": 3}
|
||||
}
|
||||
```
|
||||
|
||||
**响应(200 OK)**:
|
||||
```json
|
||||
{"job_id": "glm-ft-xxxxxx"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /api/v1/finetune/status/{jobId}
|
||||
|
||||
查询微调任务状态。
|
||||
|
||||
**路径参数**: `jobId` — 微调任务 ID(由 `/finetune/start` 返回)
|
||||
|
||||
**响应(200 OK)**:
|
||||
```json
|
||||
{
|
||||
"job_id": "glm-ft-xxxxxx",
|
||||
"status": "RUNNING",
|
||||
"progress": 45,
|
||||
"error_message": null
|
||||
}
|
||||
```
|
||||
|
||||
`status` 取值: `RUNNING` | `SUCCESS` | `FAILED`
|
||||
167
specs/001-ai-service-requirements/data-model.md
Normal file
167
specs/001-ai-service-requirements/data-model.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Data Model: AI 服务
|
||||
|
||||
**Branch**: `001-ai-service-requirements` | **Date**: 2026-04-10
|
||||
|
||||
---
|
||||
|
||||
## 实体定义
|
||||
|
||||
### TripleItem(文本三元组)
|
||||
|
||||
从文档中提取的一条知识关系。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| subject | string | 非空 | 主语实体 |
|
||||
| predicate | string | 非空 | 谓语/关系 |
|
||||
| object | string | 非空 | 宾语实体 |
|
||||
| source_snippet | string | 非空 | 原文中的证据片段(直接引用) |
|
||||
| source_offset.start | int | ≥0 | 证据片段在全文中的起始字符偏移 |
|
||||
| source_offset.end | int | >start | 证据片段在全文中的结束字符偏移 |
|
||||
|
||||
**状态转换**: 无(只读输出)
|
||||
|
||||
---
|
||||
|
||||
### QuadrupleItem(图像四元组)
|
||||
|
||||
从图像中提取的一条知识关系,带图像位置信息。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| subject | string | 非空 | 主体实体 |
|
||||
| predicate | string | 非空 | 关系/属性 |
|
||||
| object | string | 非空 | 客体实体 |
|
||||
| qualifier | string | 可为空 | 修饰信息(时间、条件、场景) |
|
||||
| bbox.x | int | ≥0 | 边界框左上角 x 像素坐标 |
|
||||
| bbox.y | int | ≥0 | 边界框左上角 y 像素坐标 |
|
||||
| bbox.w | int | >0 | 边界框宽度(像素) |
|
||||
| bbox.h | int | >0 | 边界框高度(像素) |
|
||||
| cropped_image_path | string | 非空 | 裁剪图在 RustFS 中的存储路径 |
|
||||
|
||||
**派生规则**: `cropped_image_path = "crops/{task_id}/{item_index}.jpg"`,由 image_service 自动生成并上传
|
||||
|
||||
---
|
||||
|
||||
### QAPair(文本问答对)
|
||||
|
||||
由文本三元组生成的训练候选问答对。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| question | string | 非空 | 问题文本 |
|
||||
| answer | string | 非空 | 答案文本 |
|
||||
|
||||
---
|
||||
|
||||
### ImageQAPair(图像问答对)
|
||||
|
||||
由图像四元组生成的训练候选图文问答对。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| question | string | 非空 | 问题文本 |
|
||||
| answer | string | 非空 | 答案文本 |
|
||||
| image_path | string | 非空 | 对应裁剪图的存储路径(来源于 QuadrupleItem.cropped_image_path) |
|
||||
|
||||
---
|
||||
|
||||
### FrameInfo(视频帧信息)
|
||||
|
||||
视频帧提取任务中单帧的元数据。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| frame_index | int | ≥0 | 帧在视频中的原始帧序号 |
|
||||
| time_sec | float | ≥0.0 | 帧对应的时间点(秒) |
|
||||
| frame_path | string | 非空 | 帧图在 RustFS 中的存储路径 |
|
||||
|
||||
**派生规则**: `frame_path = "frames/{source_id}/{upload_index}.jpg"`
|
||||
|
||||
---
|
||||
|
||||
### VideoJobCallback(视频任务回调)
|
||||
|
||||
异步视频任务完成后发送给 Java 后端的通知载荷。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| job_id | int | 非空 | 由 Java 后端分配的任务 ID |
|
||||
| status | string | SUCCESS \| FAILED | 任务最终状态 |
|
||||
| frames | FrameInfo[] \| null | 仅帧提取时非 null | 提取的帧列表(可为空列表) |
|
||||
| output_path | string \| null | 仅视频转文本时非 null | 输出文字描述的存储路径 |
|
||||
| error_message | string \| null | 仅 FAILED 时非 null | 错误描述 |
|
||||
|
||||
---
|
||||
|
||||
### FinetuneJob(微调任务)
|
||||
|
||||
微调任务的状态快照。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| job_id | string | 非空 | 由 ZhipuAI 平台分配的任务 ID(如 "glm-ft-xxxxxx") |
|
||||
| status | string | RUNNING \| SUCCESS \| FAILED | 当前状态 |
|
||||
| progress | int \| null | 0-100 \| null | 完成百分比(ZhipuAI 支持时) |
|
||||
| error_message | string \| null | 仅 FAILED 时非 null | 错误描述 |
|
||||
|
||||
**状态映射**:
|
||||
```
|
||||
ZhipuAI "running" → RUNNING
|
||||
ZhipuAI "succeeded" → SUCCESS
|
||||
ZhipuAI "failed" → FAILED
|
||||
其他 → RUNNING(保守处理)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RustFS 存储路径规范
|
||||
|
||||
| 资源类型 | 存储桶 | 路径格式 |
|
||||
|----------|--------|----------|
|
||||
| 上传文本文件 | `source-data` | `text/{年月}/{source_id}.txt` |
|
||||
| 上传图片 | `source-data` | `image/{年月}/{source_id}.jpg` |
|
||||
| 上传视频 | `source-data` | `video/{年月}/{source_id}.mp4` |
|
||||
| 视频帧图 | `source-data` | `frames/{source_id}/{upload_index}.jpg` |
|
||||
| 视频转译文本 | `source-data` | `video-text/{source_id}/{timestamp}.txt` |
|
||||
| 图像/帧 bbox 裁剪图 | `source-data` | `crops/{task_id}/{item_index}.jpg` |
|
||||
| 导出 JSONL 文件 | `finetune-export` | `export/{batchUuid}.jsonl` |
|
||||
|
||||
---
|
||||
|
||||
## 配置模型
|
||||
|
||||
### config.yaml(非敏感,提交 git)
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: 8000
|
||||
log_level: INFO
|
||||
|
||||
storage:
|
||||
buckets:
|
||||
source_data: "source-data"
|
||||
finetune_export: "finetune-export"
|
||||
|
||||
backend: {} # callback_url 由 .env 注入
|
||||
|
||||
video:
|
||||
frame_sample_count: 8 # 视频转文本时均匀采样帧数
|
||||
max_file_size_mb: 200 # 视频大小上限(可通过 MAX_VIDEO_SIZE_MB 覆盖)
|
||||
|
||||
models:
|
||||
default_text: "glm-4-flash"
|
||||
default_vision: "glm-4v-flash"
|
||||
```
|
||||
|
||||
### 环境变量覆盖映射
|
||||
|
||||
| 环境变量 | YAML 路径 | 说明 |
|
||||
|----------|-----------|------|
|
||||
| ZHIPUAI_API_KEY | zhipuai.api_key | 必填 |
|
||||
| STORAGE_ACCESS_KEY | storage.access_key | 必填 |
|
||||
| STORAGE_SECRET_KEY | storage.secret_key | 必填 |
|
||||
| STORAGE_ENDPOINT | storage.endpoint | RustFS 地址 |
|
||||
| BACKEND_CALLBACK_URL | backend.callback_url | Java 后端回调接口 |
|
||||
| LOG_LEVEL | server.log_level | 日志级别 |
|
||||
| MAX_VIDEO_SIZE_MB | video.max_file_size_mb | 视频大小上限 |
|
||||
120
specs/001-ai-service-requirements/plan.md
Normal file
120
specs/001-ai-service-requirements/plan.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Implementation Plan: AI 服务需求文档
|
||||
|
||||
**Branch**: `001-ai-service-requirements` | **Date**: 2026-04-10 | **Spec**: [spec.md](spec.md)
|
||||
**Input**: Feature specification from `/specs/001-ai-service-requirements/spec.md`
|
||||
|
||||
> **参考实现计划(主计划)**: `docs/superpowers/plans/2026-04-10-ai-service-impl.md`
|
||||
> 本文件为 speckit 规划框架文档,详细 TDD 任务(17 个步骤含完整代码)见上述主计划。
|
||||
|
||||
## Summary
|
||||
|
||||
实现一个独立部署的 Python FastAPI AI 服务,为知识图谱标注平台提供文本三元组提取、图像四元组提取、视频帧处理、问答对生成和 GLM 微调管理能力。服务通过 RustFS S3 API 读写文件,通过 ZhipuAI GLM API 调用大模型,通过回调接口通知 Java 后端异步任务结果。采用 ABC 适配层(LLMClient / StorageClient)保证可扩展性,FastAPI BackgroundTasks 处理视频长任务,全量 TDD 开发。
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: Python 3.12.13(conda `label` 环境)
|
||||
**Primary Dependencies**: FastAPI ≥0.111, uvicorn[standard] ≥0.29, pydantic ≥2.7, zhipuai ≥2.1, boto3 ≥1.34, pdfplumber ≥0.11, python-docx ≥1.1, opencv-python-headless ≥4.9, numpy ≥1.26, httpx ≥0.27, python-dotenv ≥1.0, pyyaml ≥6.0
|
||||
**Storage**: RustFS(S3 兼容协议,boto3 访问)
|
||||
**Testing**: pytest ≥8.0 + pytest-asyncio ≥0.23,所有 service 和 router 均有单元测试
|
||||
**Target Platform**: Linux 容器(Docker + Docker Compose)
|
||||
**Project Type**: web-service
|
||||
**Performance Goals**: 文本提取 <60s;图像提取 <30s;视频任务接受 <1s;健康检查 <1s;QA 生成(≤10条)<90s
|
||||
**Constraints**: 视频文件大小上限默认 200MB(可通过 MAX_VIDEO_SIZE_MB 环境变量配置);不访问数据库;GLM 为云端 API,图片须以 base64 传输;ZhipuAI SDK 同步阻塞,须在线程池中执行
|
||||
**Scale/Scope**: 低并发(ADMIN 手动触发),同时不超过 5 个视频任务
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
> 项目 constitution 为未填充的模板,无项目特定约束规则。以下采用通用工程原则进行评估。
|
||||
|
||||
| 原则 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 测试优先(TDD) | ✅ 通过 | 实现计划采用红绿重构循环,所有模块先写测试 |
|
||||
| 简单性(YAGNI) | ✅ 通过 | BackgroundTasks 而非 Celery;无数据库;适配层仅当前实现 |
|
||||
| 可观测性 | ✅ 通过 | JSON 结构化日志,含请求/GLM/视频任务维度 |
|
||||
| 错误分类 | ✅ 通过 | 4 种异常类(400/502/503/500),结构化响应 |
|
||||
| 可扩展性 | ✅ 通过 | LLMClient / StorageClient ABC 适配层 |
|
||||
| 配置分层 | ✅ 通过 | config.yaml + .env + 环境变量覆盖 |
|
||||
|
||||
**GATE RESULT**: ✅ 无违规,可进入 Phase 0。
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/001-ai-service-requirements/
|
||||
├── plan.md # 本文件 (/speckit.plan 输出)
|
||||
├── research.md # Phase 0 输出
|
||||
├── data-model.md # Phase 1 输出
|
||||
├── quickstart.md # Phase 1 输出
|
||||
├── contracts/ # Phase 1 输出
|
||||
│ └── api.md
|
||||
└── tasks.md # Phase 2 输出 (/speckit.tasks - 未由本命令创建)
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
label_ai_service/
|
||||
├── app/
|
||||
│ ├── main.py # FastAPI 应用入口,lifespan,/health 端点
|
||||
│ ├── core/
|
||||
│ │ ├── config.py # YAML + .env 分层配置,lru_cache 单例
|
||||
│ │ ├── logging.py # JSON 结构化日志,请求日志中间件
|
||||
│ │ ├── exceptions.py # 自定义异常 + 全局处理器
|
||||
│ │ ├── json_utils.py # GLM 响应 JSON 解析(兼容 Markdown 代码块)
|
||||
│ │ └── dependencies.py # FastAPI Depends 工厂函数
|
||||
│ ├── clients/
|
||||
│ │ ├── llm/
|
||||
│ │ │ ├── base.py # LLMClient ABC(chat / chat_vision)
|
||||
│ │ │ └── zhipuai_client.py # ZhipuAI 实现(线程池包装同步 SDK)
|
||||
│ │ └── storage/
|
||||
│ │ ├── base.py # StorageClient ABC(download/upload/presigned/size)
|
||||
│ │ └── rustfs_client.py # RustFS S3 兼容实现
|
||||
│ ├── services/
|
||||
│ │ ├── text_service.py # TXT/PDF/DOCX 解析 + 三元组提取
|
||||
│ │ ├── image_service.py # 四元组提取 + bbox 裁剪
|
||||
│ │ ├── video_service.py # 帧提取 + 视频转文本(BackgroundTask)
|
||||
│ │ ├── qa_service.py # 文本/图像问答对生成(图像用 base64)
|
||||
│ │ └── finetune_service.py # 微调任务提交与查询
|
||||
│ ├── routers/
|
||||
│ │ ├── text.py # POST /api/v1/text/extract
|
||||
│ │ ├── image.py # POST /api/v1/image/extract
|
||||
│ │ ├── video.py # POST /api/v1/video/extract-frames, /to-text
|
||||
│ │ ├── qa.py # POST /api/v1/qa/gen-text, /gen-image
|
||||
│ │ └── finetune.py # POST /api/v1/finetune/start, GET /status/{id}
|
||||
│ └── models/
|
||||
│ ├── text_models.py
|
||||
│ ├── image_models.py
|
||||
│ ├── video_models.py
|
||||
│ ├── qa_models.py
|
||||
│ └── finetune_models.py
|
||||
├── tests/
|
||||
│ ├── conftest.py # mock_llm, mock_storage fixtures
|
||||
│ ├── test_config.py
|
||||
│ ├── test_llm_client.py
|
||||
│ ├── test_storage_client.py
|
||||
│ ├── test_text_service.py
|
||||
│ ├── test_text_router.py
|
||||
│ ├── test_image_service.py
|
||||
│ ├── test_image_router.py
|
||||
│ ├── test_video_service.py
|
||||
│ ├── test_video_router.py
|
||||
│ ├── test_qa_service.py
|
||||
│ ├── test_qa_router.py
|
||||
│ ├── test_finetune_service.py
|
||||
│ └── test_finetune_router.py
|
||||
├── config.yaml
|
||||
├── .env
|
||||
├── requirements.txt
|
||||
├── Dockerfile
|
||||
└── docker-compose.yml
|
||||
```
|
||||
|
||||
**Structure Decision**: 单项目结构(Option 1),分层为 routers → services → clients,测试与源码并列。
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
> Constitution 无违规,此节无需填写。
|
||||
109
specs/001-ai-service-requirements/quickstart.md
Normal file
109
specs/001-ai-service-requirements/quickstart.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Quickstart: AI 服务开发指南
|
||||
|
||||
**Branch**: `001-ai-service-requirements` | **Date**: 2026-04-10
|
||||
|
||||
---
|
||||
|
||||
## 环境准备
|
||||
|
||||
```bash
|
||||
# 激活 conda 环境
|
||||
conda activate label
|
||||
|
||||
# 安装依赖(在 label_ai_service 目录下)
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本地开发启动
|
||||
|
||||
```bash
|
||||
# 1. 复制并配置 .env(已提交模板)
|
||||
# 编辑 .env 填写真实的 ZHIPUAI_API_KEY 和 STORAGE_ENDPOINT
|
||||
|
||||
# 2. 启动开发服务器
|
||||
conda run -n label uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
|
||||
# 3. 访问 Swagger 文档
|
||||
# http://localhost:8000/docs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
# 运行全部测试
|
||||
conda run -n label pytest tests/ -v
|
||||
|
||||
# 运行指定模块测试
|
||||
conda run -n label pytest tests/test_text_service.py -v
|
||||
|
||||
# 运行带覆盖率报告
|
||||
conda run -n label pytest tests/ --cov=app --cov-report=term-missing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker 部署
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t label-ai-service:dev .
|
||||
|
||||
# 使用 docker-compose 启动(含 RustFS)
|
||||
docker-compose up -d
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f ai-service
|
||||
|
||||
# 健康检查
|
||||
curl http://localhost:8000/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键配置说明
|
||||
|
||||
### 视频大小上限调整
|
||||
|
||||
无需重建镜像,在 `.env` 中添加:
|
||||
```ini
|
||||
MAX_VIDEO_SIZE_MB=500
|
||||
```
|
||||
|
||||
### 切换大模型
|
||||
|
||||
修改 `config.yaml`:
|
||||
```yaml
|
||||
models:
|
||||
default_text: "glm-4-flash" # 文本模型
|
||||
default_vision: "glm-4v-flash" # 视觉模型
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 开发流程(TDD)
|
||||
|
||||
详细的 17 个任务步骤(含完整代码)见主实现计划:
|
||||
`docs/superpowers/plans/2026-04-10-ai-service-impl.md`
|
||||
|
||||
每个任务的开发步骤:
|
||||
1. 编写失败测试(`pytest ... -v` 验证失败)
|
||||
2. 实现最小代码使测试通过(`pytest ... -v` 验证通过)
|
||||
3. Commit
|
||||
|
||||
---
|
||||
|
||||
## 目录结构速查
|
||||
|
||||
```
|
||||
app/
|
||||
├── main.py # 入口,/health 端点,路由注册
|
||||
├── core/ # 配置、日志、异常、工具
|
||||
├── clients/ # LLM 和 Storage 适配层(ABC + 实现)
|
||||
├── services/ # 业务逻辑(text/image/video/qa/finetune)
|
||||
├── routers/ # HTTP 路由处理
|
||||
└── models/ # Pydantic 请求/响应 Schema
|
||||
```
|
||||
76
specs/001-ai-service-requirements/research.md
Normal file
76
specs/001-ai-service-requirements/research.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Research: AI 服务实现方案
|
||||
|
||||
**Branch**: `001-ai-service-requirements` | **Date**: 2026-04-10
|
||||
**Status**: 完成(所有决策已在设计阶段确定,无待研究项)
|
||||
|
||||
---
|
||||
|
||||
## 决策记录
|
||||
|
||||
### D-001: 异步框架选型
|
||||
|
||||
**Decision**: FastAPI + uvicorn
|
||||
**Rationale**: 原生 async/await 支持、Pydantic 自动校验、自动生成 Swagger 文档、Python 生态系中性能和开发效率的最优权衡。
|
||||
**Alternatives considered**: Django(过重)、Flask(无原生异步)、aiohttp(无自动文档和类型校验)
|
||||
|
||||
---
|
||||
|
||||
### D-002: ZhipuAI SDK 调用方式
|
||||
|
||||
**Decision**: 同步 SDK 通过 `asyncio.get_event_loop().run_in_executor(None, ...)` 在线程池中调用
|
||||
**Rationale**: ZhipuAI 官方 SDK 为同步阻塞设计,直接在 async 函数中调用会阻塞事件循环。`run_in_executor` 将阻塞调用卸载到线程池,保持 FastAPI 事件循环响应能力。
|
||||
**Alternatives considered**: 使用 `asyncio.to_thread()`(Python 3.9+ 语法糖,等效实现,选择 run_in_executor 保持向后兼容性);使用 httpx 直接调用 ZhipuAI HTTP API(绕过 SDK 但增加维护负担)
|
||||
|
||||
---
|
||||
|
||||
### D-003: 图像 QA 生成的图片传输方式
|
||||
|
||||
**Decision**: base64 编码嵌入消息体(`data:image/jpeg;base64,...`)
|
||||
**Rationale**: RustFS 部署在 Docker 内网(endpoint: `http://rustfs:9000`),presigned URL 指向内网地址,云端 GLM-4V 无法访问。base64 编码将图片内容直接内联到 API 请求,不依赖网络可达性。
|
||||
**Alternatives considered**: presigned URL(不可行,内网地址云端不可达);公网 RustFS 暴露(增加安全风险)
|
||||
|
||||
---
|
||||
|
||||
### D-004: 视频长任务处理机制
|
||||
|
||||
**Decision**: FastAPI BackgroundTasks + HTTP 回调通知 Java 后端
|
||||
**Rationale**: 视频处理耗时不可控(几秒到几分钟),同步等待会超时。BackgroundTasks 无需额外中间件(Redis/Celery),部署简单,任务状态通过回调接口由 Java 后端管理,符合整体架构风格。并发量有限(≤5个同时任务),BackgroundTasks 完全够用。
|
||||
**Alternatives considered**: Celery(需 Redis broker,引入额外运维负担);asyncio.create_task(进程重启会丢失任务)
|
||||
|
||||
---
|
||||
|
||||
### D-005: 分层配置方案
|
||||
|
||||
**Decision**: config.yaml(稳定非敏感配置)+ .env(密钥和环境差异项),环境变量优先级高于 YAML
|
||||
**Rationale**: YAML 提供结构化可读性,适合 git 追踪非敏感配置变更;.env 格式为 Docker `env_file` 原生支持;环境变量覆盖机制使容器部署时无需重建镜像即可切换配置。
|
||||
**Alternatives considered**: 纯 .env 文件(缺乏结构化,复杂配置难维护);数据库存储配置(过重)
|
||||
|
||||
---
|
||||
|
||||
### D-006: 视频大文件 OOM 防护
|
||||
|
||||
**Decision**: 在视频路由层(接受请求后、启动后台任务前)通过 `storage.get_object_size()` 查询文件大小,超限返回 HTTP 400
|
||||
**Rationale**: 在下载前拒绝,避免实际 OOM;大小限制通过 config.yaml + MAX_VIDEO_SIZE_MB 环境变量运行时可配置,无需重建镜像;实现简单,无需引入流式下载的新抽象。
|
||||
**Alternatives considered**: 流式下载(Completeness: 9/10,但 YAGNI,当前规模不需要);不限制(Completeness: 4/10,有 OOM 风险)
|
||||
|
||||
---
|
||||
|
||||
### D-007: 视频关键帧检测算法
|
||||
|
||||
**Decision**: 帧差分(frame difference)近似检测:计算当前帧与前帧灰度图的像素差均值,差值超过阈值(默认 30.0)判定为场景切换
|
||||
**Rationale**: OpenCV 无原生 I 帧检测 API(`CAP_PROP_POS_FRAMES` 是帧定位,非 I 帧标识)。帧差分简单有效,对场景切换检测准确,且无需视频解码器底层支持。
|
||||
**Alternatives considered**: 基于编码信息的 I 帧检测(需 FFmpeg 支持,引入额外依赖);固定间隔(不够智能,不适合关键帧模式)
|
||||
|
||||
---
|
||||
|
||||
### D-008: 测试策略
|
||||
|
||||
**Decision**: pytest + pytest-asyncio,Service 层和 Router 层分别测试,使用 AsyncMock 模拟外部依赖
|
||||
**Rationale**: Service 层测试业务逻辑,不依赖 HTTP;Router 层使用 TestClient 测试完整请求流程。视频 service 测试使用真实小视频文件(OpenCV VideoWriter 生成),验证帧提取逻辑正确性。
|
||||
**Alternatives considered**: 仅集成测试(需要真实 RustFS 和 ZhipuAI,CI 成本高);全部单元测试(无法覆盖路由和异常处理器集成)
|
||||
|
||||
---
|
||||
|
||||
## 无待解决项
|
||||
|
||||
所有 NEEDS CLARIFICATION 均已在设计阶段通过用户确认或合理默认值解决。本 research.md 仅作决策存档。
|
||||
Reference in New Issue
Block a user