mirror of
https://github.com/TheSmallHanCat/sora2api.git
synced 2026-03-15 00:17:31 +08:00
feat: 新增每日用量统计 fix: 修复视频参数
This commit is contained in:
16
README.md
16
README.md
@@ -126,8 +126,8 @@ python main.py
|
|||||||
| 图生图 | `sora-image*` | 使用 `content` 数组 + `image_url` |
|
| 图生图 | `sora-image*` | 使用 `content` 数组 + `image_url` |
|
||||||
| 文生视频 | `sora-video*` | 使用 `content` 为字符串 |
|
| 文生视频 | `sora-video*` | 使用 `content` 为字符串 |
|
||||||
| 图生视频 | `sora-video*` | 使用 `content` 数组 + `image_url` |
|
| 图生视频 | `sora-video*` | 使用 `content` 数组 + `image_url` |
|
||||||
| 创建角色 | `sora-video*` | 使用 `content` 数组 + `input_video` |
|
| 创建角色 | `sora-video*` | 使用 `content` 数组 + `video_url` |
|
||||||
| 角色生成视频 | `sora-video*` | 使用 `content` 数组 + `input_video` + 文本 |
|
| 角色生成视频 | `sora-video*` | 使用 `content` 数组 + `video_url` + 文本 |
|
||||||
| Remix | `sora-video*` | 在 `content` 中包含 Remix ID |
|
| Remix | `sora-video*` | 在 `content` 中包含 Remix ID |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -299,8 +299,8 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \
|
|||||||
"role": "user",
|
"role": "user",
|
||||||
"content": [
|
"content": [
|
||||||
{
|
{
|
||||||
"type": "input_video",
|
"type": "video_url",
|
||||||
"videoUrl": {
|
"video_url": {
|
||||||
"url": "data:video/mp4;base64,<base64_encoded_video_data>"
|
"url": "data:video/mp4;base64,<base64_encoded_video_data>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,8 +326,8 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \
|
|||||||
"role": "user",
|
"role": "user",
|
||||||
"content": [
|
"content": [
|
||||||
{
|
{
|
||||||
"type": "input_video",
|
"type": "video_url",
|
||||||
"videoUrl": {
|
"video_url": {
|
||||||
"url": "data:video/mp4;base64,<base64_encoded_video_data>"
|
"url": "data:video/mp4;base64,<base64_encoded_video_data>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -366,8 +366,8 @@ response = requests.post(
|
|||||||
"role": "user",
|
"role": "user",
|
||||||
"content": [
|
"content": [
|
||||||
{
|
{
|
||||||
"type": "input_video",
|
"type": "video_url",
|
||||||
"videoUrl": {
|
"video_url": {
|
||||||
"url": f"data:video/mp4;base64,{video_data}"
|
"url": f"data:video/mp4;base64,{video_data}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -480,24 +480,33 @@ async def get_stats(token: str = Depends(verify_admin_token)):
|
|||||||
"""Get system statistics"""
|
"""Get system statistics"""
|
||||||
tokens = await token_manager.get_all_tokens()
|
tokens = await token_manager.get_all_tokens()
|
||||||
active_tokens = await token_manager.get_active_tokens()
|
active_tokens = await token_manager.get_active_tokens()
|
||||||
|
|
||||||
total_images = 0
|
total_images = 0
|
||||||
total_videos = 0
|
total_videos = 0
|
||||||
total_errors = 0
|
total_errors = 0
|
||||||
|
today_images = 0
|
||||||
|
today_videos = 0
|
||||||
|
today_errors = 0
|
||||||
|
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
stats = await db.get_token_stats(token.id)
|
stats = await db.get_token_stats(token.id)
|
||||||
if stats:
|
if stats:
|
||||||
total_images += stats.image_count
|
total_images += stats.image_count
|
||||||
total_videos += stats.video_count
|
total_videos += stats.video_count
|
||||||
total_errors += stats.error_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 {
|
return {
|
||||||
"total_tokens": len(tokens),
|
"total_tokens": len(tokens),
|
||||||
"active_tokens": len(active_tokens),
|
"active_tokens": len(active_tokens),
|
||||||
"total_images": total_images,
|
"total_images": total_images,
|
||||||
"total_videos": total_videos,
|
"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
|
# Sora2 endpoints
|
||||||
|
|||||||
@@ -111,9 +111,9 @@ async def create_chat_completion(
|
|||||||
image_data = url.split("base64,", 1)[1]
|
image_data = url.split("base64,", 1)[1]
|
||||||
else:
|
else:
|
||||||
image_data = url
|
image_data = url
|
||||||
elif item.get("type") == "input_video":
|
elif item.get("type") == "video_url":
|
||||||
# Extract video from input_video
|
# Extract video from video_url
|
||||||
video_url = item.get("videoUrl", {})
|
video_url = item.get("video_url", {})
|
||||||
url = video_url.get("url", "")
|
url = video_url.get("url", "")
|
||||||
if url.startswith("data:video") or url.startswith("data:application"):
|
if url.startswith("data:video") or url.startswith("data:application"):
|
||||||
# Extract base64 data from data URI
|
# Extract base64 data from data URI
|
||||||
|
|||||||
@@ -283,6 +283,10 @@ class Database:
|
|||||||
video_count INTEGER DEFAULT 0,
|
video_count INTEGER DEFAULT 0,
|
||||||
error_count INTEGER DEFAULT 0,
|
error_count INTEGER DEFAULT 0,
|
||||||
last_error_at TIMESTAMP,
|
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)
|
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_task_status ON tasks(status)")
|
||||||
await db.execute("CREATE INDEX IF NOT EXISTS idx_token_active ON tokens(is_active)")
|
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()
|
await db.commit()
|
||||||
|
|
||||||
async def init_config_from_toml(self, config_dict: dict, is_first_startup: bool = True):
|
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):
|
async def increment_image_count(self, token_id: int):
|
||||||
"""Increment image generation count"""
|
"""Increment image generation count"""
|
||||||
|
from datetime import date
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
await db.execute("""
|
today = str(date.today())
|
||||||
UPDATE token_stats SET image_count = image_count + 1 WHERE token_id = ?
|
# Get current stats
|
||||||
""", (token_id,))
|
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()
|
await db.commit()
|
||||||
|
|
||||||
async def increment_video_count(self, token_id: int):
|
async def increment_video_count(self, token_id: int):
|
||||||
"""Increment video generation count"""
|
"""Increment video generation count"""
|
||||||
|
from datetime import date
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
await db.execute("""
|
today = str(date.today())
|
||||||
UPDATE token_stats SET video_count = video_count + 1 WHERE token_id = ?
|
# Get current stats
|
||||||
""", (token_id,))
|
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()
|
await db.commit()
|
||||||
|
|
||||||
async def increment_error_count(self, token_id: int):
|
async def increment_error_count(self, token_id: int):
|
||||||
"""Increment error count"""
|
"""Increment error count"""
|
||||||
|
from datetime import date
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
await db.execute("""
|
today = str(date.today())
|
||||||
UPDATE token_stats
|
# Get current stats
|
||||||
SET error_count = error_count + 1, last_error_at = CURRENT_TIMESTAMP
|
cursor = await db.execute("SELECT today_date FROM token_stats WHERE token_id = ?", (token_id,))
|
||||||
WHERE token_id = ?
|
row = await cursor.fetchone()
|
||||||
""", (token_id,))
|
|
||||||
|
# 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()
|
await db.commit()
|
||||||
|
|
||||||
async def reset_error_count(self, token_id: int):
|
async def reset_error_count(self, token_id: int):
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ class TokenStats(BaseModel):
|
|||||||
video_count: int = 0
|
video_count: int = 0
|
||||||
error_count: int = 0
|
error_count: int = 0
|
||||||
last_error_at: Optional[datetime] = None
|
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):
|
class Task(BaseModel):
|
||||||
"""Task model"""
|
"""Task model"""
|
||||||
|
|||||||
@@ -62,15 +62,15 @@
|
|||||||
<h3 class="text-xl font-bold text-green-600" id="statActive">-</h3>
|
<h3 class="text-xl font-bold text-green-600" id="statActive">-</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg border border-border bg-background p-4">
|
<div class="rounded-lg border border-border bg-background p-4">
|
||||||
<p class="text-sm font-medium text-muted-foreground mb-2">总图片数</p>
|
<p class="text-sm font-medium text-muted-foreground mb-2">今日图片/总图片</p>
|
||||||
<h3 class="text-xl font-bold text-blue-600" id="statImages">-</h3>
|
<h3 class="text-xl font-bold text-blue-600" id="statImages">-</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg border border-border bg-background p-4">
|
<div class="rounded-lg border border-border bg-background p-4">
|
||||||
<p class="text-sm font-medium text-muted-foreground mb-2">总视频数</p>
|
<p class="text-sm font-medium text-muted-foreground mb-2">今日视频/总视频</p>
|
||||||
<h3 class="text-xl font-bold text-purple-600" id="statVideos">-</h3>
|
<h3 class="text-xl font-bold text-purple-600" id="statVideos">-</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg border border-border bg-background p-4">
|
<div class="rounded-lg border border-border bg-background p-4">
|
||||||
<p class="text-sm font-medium text-muted-foreground mb-2">错误次数</p>
|
<p class="text-sm font-medium text-muted-foreground mb-2">今日错误/总错误</p>
|
||||||
<h3 class="text-xl font-bold text-destructive" id="statErrors">-</h3>
|
<h3 class="text-xl font-bold text-destructive" id="statErrors">-</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
<p class="text-xs text-muted-foreground mt-1">文件缓存超时时间,范围:60-86400 秒(1分钟-24小时)</p>
|
<p class="text-xs text-muted-foreground mt-1">文件缓存超时时间,范围:60-86400 秒(1分钟-24小时)</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-sm font-medium mb-2 block">缓存文件访问域名(请使用当前服务的地址)</label>
|
<label class="text-sm font-medium mb-2 block">缓存文件访问域名</label>
|
||||||
<input id="cfgCacheBaseUrl" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="https://yourdomain.com">
|
<input id="cfgCacheBaseUrl" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="https://yourdomain.com">
|
||||||
<p class="text-xs text-muted-foreground mt-1">留空则使用服务器地址,例如:https://yourdomain.com</p>
|
<p class="text-xs text-muted-foreground mt-1">留空则使用服务器地址,例如:https://yourdomain.com</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -271,7 +271,7 @@
|
|||||||
<input type="checkbox" id="cfgWatermarkFreeEnabled" class="h-4 w-4 rounded border-input" onchange="toggleWatermarkFreeOptions()">
|
<input type="checkbox" id="cfgWatermarkFreeEnabled" class="h-4 w-4 rounded border-input" onchange="toggleWatermarkFreeOptions()">
|
||||||
<span class="text-sm font-medium">开启无水印模式</span>
|
<span class="text-sm font-medium">开启无水印模式</span>
|
||||||
</label>
|
</label>
|
||||||
<p class="text-xs text-muted-foreground mt-2">开启后生成的视频将会被发布到sora平台并且提取返回无水印的视频,在缓存到本地后会自动删除发布的视频(需要开启缓存功能)</p>
|
<p class="text-xs text-muted-foreground mt-2">开启后生成的视频将会被发布到sora平台并且提取返回无水印的视频,在缓存到本地后会自动删除发布的视频</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 解析方式选择 -->
|
<!-- 解析方式选择 -->
|
||||||
@@ -561,7 +561,7 @@
|
|||||||
const $=(id)=>document.getElementById(id),
|
const $=(id)=>document.getElementById(id),
|
||||||
checkAuth=()=>{const t=localStorage.getItem('adminToken');return t||(location.href='/login',null),t},
|
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},
|
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)}},
|
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`<span class="text-red-600">${dateStr} ${timeStr}</span>`;const days=Math.floor(diff/864e5);if(days<7)return`<span class="text-orange-600">${dateStr} ${timeStr}</span>`;return`${dateStr} ${timeStr}`},
|
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`<span class="text-red-600">${dateStr} ${timeStr}</span>`;const days=Math.floor(diff/864e5);if(days<7)return`<span class="text-orange-600">${dateStr} ${timeStr}</span>`;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},
|
formatPlanType=type=>{if(!type)return'-';const typeMap={'chatgpt_team':'Team','chatgpt_plus':'Plus','chatgpt_pro':'Pro','chatgpt_free':'Free'};return typeMap[type]||type},
|
||||||
|
|||||||
Reference in New Issue
Block a user