From 85f5c3620eb1886f09539b34c42545af9939be8c Mon Sep 17 00:00:00 2001 From: TheSmallHanCat Date: Tue, 18 Nov 2025 09:14:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=AF=8F=E6=97=A5?= =?UTF-8?q?=E7=94=A8=E9=87=8F=E7=BB=9F=E8=AE=A1=20=20fix:=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E8=A7=86=E9=A2=91=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 +++---- src/api/admin.py | 17 ++++++-- src/api/routes.py | 6 +-- src/core/database.py | 101 ++++++++++++++++++++++++++++++++++++++----- src/core/models.py | 4 ++ static/manage.html | 12 ++--- 6 files changed, 123 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index ea41e7c..ded361f 100644 --- a/README.md +++ b/README.md @@ -126,8 +126,8 @@ python main.py | 图生图 | `sora-image*` | 使用 `content` 数组 + `image_url` | | 文生视频 | `sora-video*` | 使用 `content` 为字符串 | | 图生视频 | `sora-video*` | 使用 `content` 数组 + `image_url` | -| 创建角色 | `sora-video*` | 使用 `content` 数组 + `input_video` | -| 角色生成视频 | `sora-video*` | 使用 `content` 数组 + `input_video` + 文本 | +| 创建角色 | `sora-video*` | 使用 `content` 数组 + `video_url` | +| 角色生成视频 | `sora-video*` | 使用 `content` 数组 + `video_url` + 文本 | | Remix | `sora-video*` | 在 `content` 中包含 Remix ID | --- @@ -299,8 +299,8 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \ "role": "user", "content": [ { - "type": "input_video", - "videoUrl": { + "type": "video_url", + "video_url": { "url": "data:video/mp4;base64," } } @@ -326,8 +326,8 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \ "role": "user", "content": [ { - "type": "input_video", - "videoUrl": { + "type": "video_url", + "video_url": { "url": "data:video/mp4;base64," } }, @@ -366,8 +366,8 @@ response = requests.post( "role": "user", "content": [ { - "type": "input_video", - "videoUrl": { + "type": "video_url", + "video_url": { "url": f"data:video/mp4;base64,{video_data}" } } diff --git a/src/api/admin.py b/src/api/admin.py index 48e719d..6fdaba1 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -480,24 +480,33 @@ async def get_stats(token: str = Depends(verify_admin_token)): """Get system statistics""" tokens = await token_manager.get_all_tokens() active_tokens = await token_manager.get_active_tokens() - + total_images = 0 total_videos = 0 total_errors = 0 - + today_images = 0 + today_videos = 0 + today_errors = 0 + for token in tokens: stats = await db.get_token_stats(token.id) if stats: total_images += stats.image_count total_videos += stats.video_count total_errors += stats.error_count - + today_images += stats.today_image_count + today_videos += stats.today_video_count + today_errors += stats.today_error_count + return { "total_tokens": len(tokens), "active_tokens": len(active_tokens), "total_images": total_images, "total_videos": total_videos, - "total_errors": total_errors + "today_images": today_images, + "today_videos": today_videos, + "total_errors": total_errors, + "today_errors": today_errors } # Sora2 endpoints diff --git a/src/api/routes.py b/src/api/routes.py index 90b0135..99cf53f 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -111,9 +111,9 @@ async def create_chat_completion( image_data = url.split("base64,", 1)[1] else: image_data = url - elif item.get("type") == "input_video": - # Extract video from input_video - video_url = item.get("videoUrl", {}) + elif item.get("type") == "video_url": + # Extract video from video_url + video_url = item.get("video_url", {}) url = video_url.get("url", "") if url.startswith("data:video") or url.startswith("data:application"): # Extract base64 data from data URI diff --git a/src/core/database.py b/src/core/database.py index 2e8f12c..f7251dc 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -283,6 +283,10 @@ class Database: video_count INTEGER DEFAULT 0, error_count INTEGER DEFAULT 0, last_error_at TIMESTAMP, + today_image_count INTEGER DEFAULT 0, + today_video_count INTEGER DEFAULT 0, + today_error_count INTEGER DEFAULT 0, + today_date DATE, FOREIGN KEY (token_id) REFERENCES tokens(id) ) """) @@ -393,6 +397,16 @@ class Database: await db.execute("CREATE INDEX IF NOT EXISTS idx_task_status ON tasks(status)") await db.execute("CREATE INDEX IF NOT EXISTS idx_token_active ON tokens(is_active)") + # Migration: Add daily statistics columns if they don't exist + if not await self._column_exists(db, "token_stats", "today_image_count"): + await db.execute("ALTER TABLE token_stats ADD COLUMN today_image_count INTEGER DEFAULT 0") + if not await self._column_exists(db, "token_stats", "today_video_count"): + await db.execute("ALTER TABLE token_stats ADD COLUMN today_video_count INTEGER DEFAULT 0") + if not await self._column_exists(db, "token_stats", "today_error_count"): + await db.execute("ALTER TABLE token_stats ADD COLUMN today_error_count INTEGER DEFAULT 0") + if not await self._column_exists(db, "token_stats", "today_date"): + await db.execute("ALTER TABLE token_stats ADD COLUMN today_date DATE") + await db.commit() async def init_config_from_toml(self, config_dict: dict, is_first_startup: bool = True): @@ -729,28 +743,91 @@ class Database: async def increment_image_count(self, token_id: int): """Increment image generation count""" + from datetime import date async with aiosqlite.connect(self.db_path) as db: - await db.execute(""" - UPDATE token_stats SET image_count = image_count + 1 WHERE token_id = ? - """, (token_id,)) + today = str(date.today()) + # Get current stats + cursor = await db.execute("SELECT today_date FROM token_stats WHERE token_id = ?", (token_id,)) + row = await cursor.fetchone() + + # If date changed, reset today's count + if row and row[0] != today: + await db.execute(""" + UPDATE token_stats + SET image_count = image_count + 1, + today_image_count = 1, + today_date = ? + WHERE token_id = ? + """, (today, token_id)) + else: + # Same day, just increment both + await db.execute(""" + UPDATE token_stats + SET image_count = image_count + 1, + today_image_count = today_image_count + 1, + today_date = ? + WHERE token_id = ? + """, (today, token_id)) await db.commit() - + async def increment_video_count(self, token_id: int): """Increment video generation count""" + from datetime import date async with aiosqlite.connect(self.db_path) as db: - await db.execute(""" - UPDATE token_stats SET video_count = video_count + 1 WHERE token_id = ? - """, (token_id,)) + today = str(date.today()) + # Get current stats + cursor = await db.execute("SELECT today_date FROM token_stats WHERE token_id = ?", (token_id,)) + row = await cursor.fetchone() + + # If date changed, reset today's count + if row and row[0] != today: + await db.execute(""" + UPDATE token_stats + SET video_count = video_count + 1, + today_video_count = 1, + today_date = ? + WHERE token_id = ? + """, (today, token_id)) + else: + # Same day, just increment both + await db.execute(""" + UPDATE token_stats + SET video_count = video_count + 1, + today_video_count = today_video_count + 1, + today_date = ? + WHERE token_id = ? + """, (today, token_id)) await db.commit() async def increment_error_count(self, token_id: int): """Increment error count""" + from datetime import date async with aiosqlite.connect(self.db_path) as db: - await db.execute(""" - UPDATE token_stats - SET error_count = error_count + 1, last_error_at = CURRENT_TIMESTAMP - WHERE token_id = ? - """, (token_id,)) + today = str(date.today()) + # Get current stats + cursor = await db.execute("SELECT today_date FROM token_stats WHERE token_id = ?", (token_id,)) + row = await cursor.fetchone() + + # If date changed, reset today's error count + if row and row[0] != today: + await db.execute(""" + UPDATE token_stats + SET error_count = error_count + 1, + today_error_count = 1, + today_date = ?, + last_error_at = CURRENT_TIMESTAMP + WHERE token_id = ? + """, (today, token_id)) + else: + # Same day, just increment both + await db.execute(""" + UPDATE token_stats + SET error_count = error_count + 1, + today_error_count = today_error_count + 1, + today_date = ?, + last_error_at = CURRENT_TIMESTAMP + WHERE token_id = ? + """, (today, token_id)) await db.commit() async def reset_error_count(self, token_id: int): diff --git a/src/core/models.py b/src/core/models.py index fe75800..04b8772 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -42,6 +42,10 @@ class TokenStats(BaseModel): video_count: int = 0 error_count: int = 0 last_error_at: Optional[datetime] = None + today_image_count: int = 0 + today_video_count: int = 0 + today_error_count: int = 0 + today_date: Optional[str] = None class Task(BaseModel): """Task model""" diff --git a/static/manage.html b/static/manage.html index 792f1c0..4f63009 100644 --- a/static/manage.html +++ b/static/manage.html @@ -62,15 +62,15 @@

-

-

总图片数

+

今日图片/总图片

-

-

总视频数

+

今日视频/总视频

-

-

错误次数

+

今日错误/总错误

-

@@ -229,7 +229,7 @@

文件缓存超时时间,范围:60-86400 秒(1分钟-24小时)

- +

留空则使用服务器地址,例如:https://yourdomain.com

@@ -271,7 +271,7 @@ 开启无水印模式 -

开启后生成的视频将会被发布到sora平台并且提取返回无水印的视频,在缓存到本地后会自动删除发布的视频(需要开启缓存功能)

+

开启后生成的视频将会被发布到sora平台并且提取返回无水印的视频,在缓存到本地后会自动删除发布的视频

@@ -561,7 +561,7 @@ const $=(id)=>document.getElementById(id), checkAuth=()=>{const t=localStorage.getItem('adminToken');return t||(location.href='/login',null),t}, apiRequest=async(url,opts={})=>{const t=checkAuth();if(!t)return null;const r=await fetch(url,{...opts,headers:{...opts.headers,Authorization:`Bearer ${t}`,'Content-Type':'application/json'}});return r.status===401?(localStorage.removeItem('adminToken'),location.href='/login',null):r}, - loadStats=async()=>{try{const r=await apiRequest('/api/stats');if(!r)return;const d=await r.json();$('statTotal').textContent=d.total_tokens||0;$('statActive').textContent=d.active_tokens||0;$('statImages').textContent=d.total_images||0;$('statVideos').textContent=d.total_videos||0;$('statErrors').textContent=d.total_errors||0}catch(e){console.error('加载统计失败:',e)}}, + loadStats=async()=>{try{const r=await apiRequest('/api/stats');if(!r)return;const d=await r.json();$('statTotal').textContent=d.total_tokens||0;$('statActive').textContent=d.active_tokens||0;$('statImages').textContent=(d.today_images||0)+'/'+(d.total_images||0);$('statVideos').textContent=(d.today_videos||0)+'/'+(d.total_videos||0);$('statErrors').textContent=(d.today_errors||0)+'/'+(d.total_errors||0)}catch(e){console.error('加载统计失败:',e)}}, loadTokens=async()=>{try{const r=await apiRequest('/api/tokens');if(!r)return;allTokens=await r.json();renderTokens()}catch(e){console.error('加载Token失败:',e)}}, formatExpiry=exp=>{if(!exp)return'-';const d=new Date(exp),now=new Date(),diff=d-now;const dateStr=d.toLocaleDateString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit'}).replace(/\//g,'-');const timeStr=d.toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',hour12:false});if(diff<0)return`${dateStr} ${timeStr}`;const days=Math.floor(diff/864e5);if(days<7)return`${dateStr} ${timeStr}`;return`${dateStr} ${timeStr}`}, formatPlanType=type=>{if(!type)return'-';const typeMap={'chatgpt_team':'Team','chatgpt_plus':'Plus','chatgpt_pro':'Pro','chatgpt_free':'Free'};return typeMap[type]||type},