auto-save 2026-04-13 19:12 (+1)
This commit is contained in:
25
api/.env.example
Normal file
25
api/.env.example
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# MeetNote API config
|
||||||
|
|
||||||
|
# 数据库 - 本地 SQLite,生产 PostgreSQL
|
||||||
|
DATABASE_URL=sqlite+aiosqlite:///./meetnote.db
|
||||||
|
# DATABASE_URL=postgresql+asyncpg://user:pass@127.0.0.1:5432/meetnote
|
||||||
|
|
||||||
|
# 对象存储 (MinIO)
|
||||||
|
MINIO_ENDPOINT=127.0.0.1:9000
|
||||||
|
MINIO_REGION=us-east-1
|
||||||
|
MINIO_BUCKET=meetnote
|
||||||
|
MINIO_ACCESS_KEY=minioadmin
|
||||||
|
MINIO_SECRET_KEY=minioadmin
|
||||||
|
MINIO_SECURE=false
|
||||||
|
MINIO_PUBLIC_ENDPOINT=http://127.0.0.1:9000
|
||||||
|
|
||||||
|
# Groq Whisper
|
||||||
|
GROQ_API_KEY=
|
||||||
|
GROQ_MODEL=whisper-large-v3
|
||||||
|
|
||||||
|
# Poe Claude (复用全局)
|
||||||
|
POE_API_KEY=
|
||||||
|
POE_MODEL=Claude-Sonnet-4.6
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGINS=http://localhost:4490,http://192.168.2.69:4490
|
||||||
0
api/app/__init__.py
Normal file
0
api/app/__init__.py
Normal file
30
api/app/config.py
Normal file
30
api/app/config.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||||||
|
|
||||||
|
database_url: str = "sqlite+aiosqlite:///./meetnote.db"
|
||||||
|
|
||||||
|
minio_endpoint: str = "127.0.0.1:9000"
|
||||||
|
minio_region: str = "us-east-1"
|
||||||
|
minio_bucket: str = "meetnote"
|
||||||
|
minio_access_key: str = "minioadmin"
|
||||||
|
minio_secret_key: str = "minioadmin"
|
||||||
|
minio_secure: bool = False
|
||||||
|
minio_public_endpoint: str = "http://127.0.0.1:9000"
|
||||||
|
|
||||||
|
groq_api_key: str = ""
|
||||||
|
groq_model: str = "whisper-large-v3"
|
||||||
|
|
||||||
|
poe_api_key: str = ""
|
||||||
|
poe_model: str = "Claude-Sonnet-4.6"
|
||||||
|
|
||||||
|
cors_origins: str = "http://localhost:4490"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cors_origins_list(self) -> list[str]:
|
||||||
|
return [o.strip() for o in self.cors_origins.split(",") if o.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
19
api/app/db.py
Normal file
19
api/app/db.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
from .config import settings
|
||||||
|
|
||||||
|
engine = create_async_engine(settings.database_url, echo=False, future=True)
|
||||||
|
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def init_db() -> None:
|
||||||
|
# Import models so SQLModel.metadata sees them
|
||||||
|
from . import models # noqa: F401
|
||||||
|
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(SQLModel.metadata.create_all)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_session() -> AsyncSession:
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
yield session
|
||||||
51
api/app/models.py
Normal file
51
api/app/models.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
from sqlmodel import SQLModel, Field, Column
|
||||||
|
from sqlalchemy import JSON
|
||||||
|
|
||||||
|
|
||||||
|
class MeetingStatus(str, Enum):
|
||||||
|
pending = "pending"
|
||||||
|
uploading = "uploading"
|
||||||
|
uploaded = "uploaded"
|
||||||
|
splitting = "splitting"
|
||||||
|
transcribing = "transcribing"
|
||||||
|
summarizing = "summarizing"
|
||||||
|
done = "done"
|
||||||
|
failed = "failed"
|
||||||
|
|
||||||
|
|
||||||
|
class Meeting(SQLModel, table=True):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
title: str
|
||||||
|
participants: Optional[str] = None # comma-separated
|
||||||
|
object_key: Optional[str] = None # MinIO key
|
||||||
|
file_size: Optional[int] = None
|
||||||
|
duration: Optional[int] = None # seconds
|
||||||
|
status: MeetingStatus = Field(default=MeetingStatus.pending)
|
||||||
|
chunks_done: int = 0
|
||||||
|
chunks_total: int = 0
|
||||||
|
error: Optional[str] = None
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
class TranscriptSegment(SQLModel, table=True):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
meeting_id: int = Field(foreign_key="meeting.id", index=True)
|
||||||
|
start: float # seconds
|
||||||
|
end: float
|
||||||
|
speaker: Optional[str] = None
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
|
class Summary(SQLModel, table=True):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
meeting_id: int = Field(foreign_key="meeting.id", unique=True, index=True)
|
||||||
|
key_points: list = Field(default_factory=list, sa_column=Column(JSON))
|
||||||
|
todos: list = Field(default_factory=list, sa_column=Column(JSON))
|
||||||
|
decisions: list = Field(default_factory=list, sa_column=Column(JSON))
|
||||||
|
keywords: list = Field(default_factory=list, sa_column=Column(JSON))
|
||||||
|
preview: str = ""
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
0
api/app/services/__init__.py
Normal file
0
api/app/services/__init__.py
Normal file
60
api/app/services/storage.py
Normal file
60
api/app/services/storage.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""MinIO / S3 presigned URL helper.
|
||||||
|
|
||||||
|
We use boto3 because MinIO speaks the S3 protocol and boto3 is the de-facto
|
||||||
|
client. Generating presigned multipart upload URLs lets the browser upload
|
||||||
|
files >100MB directly to MinIO without going through the FastAPI process,
|
||||||
|
which avoids body-size limits and streaming-into-RAM issues.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
from botocore.client import Config
|
||||||
|
from ..config import settings
|
||||||
|
|
||||||
|
|
||||||
|
def s3_client():
|
||||||
|
return boto3.client(
|
||||||
|
"s3",
|
||||||
|
endpoint_url=("https" if settings.minio_secure else "http")
|
||||||
|
+ "://"
|
||||||
|
+ settings.minio_endpoint,
|
||||||
|
aws_access_key_id=settings.minio_access_key,
|
||||||
|
aws_secret_access_key=settings.minio_secret_key,
|
||||||
|
region_name=settings.minio_region,
|
||||||
|
config=Config(signature_version="s3v4", s3={"addressing_style": "path"}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_bucket() -> None:
|
||||||
|
client = s3_client()
|
||||||
|
try:
|
||||||
|
client.head_bucket(Bucket=settings.minio_bucket)
|
||||||
|
except Exception:
|
||||||
|
client.create_bucket(Bucket=settings.minio_bucket)
|
||||||
|
|
||||||
|
|
||||||
|
def presign_put(object_key: str, content_type: str, expires: int = 3600) -> str:
|
||||||
|
"""Single-PUT presigned URL — for files small enough not to need multipart.
|
||||||
|
|
||||||
|
For multipart (files >100MB), the frontend should use the AWS SDK's
|
||||||
|
@aws-sdk/lib-storage Upload helper, which can sign each part itself once
|
||||||
|
we hand it the credentials. For MVP we keep things simple with single-PUT
|
||||||
|
+ a 500 MB cap.
|
||||||
|
"""
|
||||||
|
return s3_client().generate_presigned_url(
|
||||||
|
"put_object",
|
||||||
|
Params={
|
||||||
|
"Bucket": settings.minio_bucket,
|
||||||
|
"Key": object_key,
|
||||||
|
"ContentType": content_type,
|
||||||
|
},
|
||||||
|
ExpiresIn=expires,
|
||||||
|
HttpMethod="PUT",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def presign_get(object_key: str, expires: int = 3600) -> str:
|
||||||
|
return s3_client().generate_presigned_url(
|
||||||
|
"get_object",
|
||||||
|
Params={"Bucket": settings.minio_bucket, "Key": object_key},
|
||||||
|
ExpiresIn=expires,
|
||||||
|
)
|
||||||
12
api/requirements.txt
Normal file
12
api/requirements.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
fastapi==0.115.6
|
||||||
|
uvicorn[standard]==0.34.0
|
||||||
|
sqlmodel==0.0.22
|
||||||
|
aiosqlite==0.20.0
|
||||||
|
asyncpg==0.30.0
|
||||||
|
greenlet==3.1.1
|
||||||
|
python-multipart==0.0.20
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
httpx==0.28.1
|
||||||
|
boto3==1.35.93
|
||||||
|
pydantic==2.10.4
|
||||||
|
pydantic-settings==2.7.0
|
||||||
Reference in New Issue
Block a user