feat: add translation billing stats and remove Export/Settings pages

- Add TranslationLog model to track translation API usage per task
- Integrate Dify API actual price (total_price) into translation stats
- Display translation statistics in admin dashboard with per-task costs
- Remove unused Export and Settings pages to simplify frontend
- Add GET /api/v2/admin/translation-stats endpoint

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-12 17:38:12 +08:00
parent d20751d56b
commit 65abd51d60
21 changed files with 682 additions and 662 deletions

View File

@@ -13,6 +13,7 @@ from app.models.user import User
from app.models.task import Task, TaskStatus
from app.models.session import Session as UserSession
from app.models.audit_log import AuditLog
from app.models.translation_log import TranslationLog
from app.core.config import settings
logger = logging.getLogger(__name__)
@@ -209,6 +210,87 @@ class AdminService:
return top_users
def get_translation_statistics(self, db: Session) -> dict:
"""
Get translation usage statistics for admin dashboard.
Args:
db: Database session
Returns:
Dictionary with translation stats including total tokens, costs, and breakdowns
"""
# Total translation count
total_translations = db.query(TranslationLog).count()
# Sum of tokens
token_stats = db.query(
func.sum(TranslationLog.total_tokens).label("total_tokens"),
func.sum(TranslationLog.input_tokens).label("total_input_tokens"),
func.sum(TranslationLog.output_tokens).label("total_output_tokens"),
func.sum(TranslationLog.total_characters).label("total_characters"),
func.sum(TranslationLog.estimated_cost).label("total_cost")
).first()
# Breakdown by target language
by_language = db.query(
TranslationLog.target_lang,
func.count(TranslationLog.id).label("count"),
func.sum(TranslationLog.total_tokens).label("tokens"),
func.sum(TranslationLog.total_characters).label("characters")
).group_by(TranslationLog.target_lang).all()
language_breakdown = [
{
"language": lang,
"count": count,
"tokens": tokens or 0,
"characters": chars or 0
}
for lang, count, tokens, chars in by_language
]
# Recent translations (last 20)
recent = db.query(TranslationLog).order_by(
TranslationLog.created_at.desc()
).limit(20).all()
recent_translations = [
{
"id": log.id,
"task_id": log.task_id,
"target_lang": log.target_lang,
"total_tokens": log.total_tokens,
"total_characters": log.total_characters,
"processing_time_seconds": log.processing_time_seconds,
"estimated_cost": log.estimated_cost,
"created_at": log.created_at.isoformat() if log.created_at else None
}
for log in recent
]
# Stats for last 30 days
date_30_days_ago = datetime.utcnow() - timedelta(days=30)
recent_stats = db.query(
func.count(TranslationLog.id).label("count"),
func.sum(TranslationLog.total_tokens).label("tokens")
).filter(TranslationLog.created_at >= date_30_days_ago).first()
return {
"total_translations": total_translations,
"total_tokens": token_stats.total_tokens or 0,
"total_input_tokens": token_stats.total_input_tokens or 0,
"total_output_tokens": token_stats.total_output_tokens or 0,
"total_characters": token_stats.total_characters or 0,
"estimated_cost": token_stats.total_cost or 0.0,
"by_language": language_breakdown,
"recent_translations": recent_translations,
"last_30_days": {
"count": recent_stats.count or 0,
"tokens": recent_stats.tokens or 0
}
}
# Singleton instance
admin_service = AdminService()

View File

@@ -40,6 +40,8 @@ class TranslationResponse:
total_tokens: int
latency: float
conversation_id: str
total_price: float = 0.0
currency: str = "USD"
@dataclass
@@ -50,6 +52,8 @@ class BatchTranslationResponse:
latency: float
conversation_id: str
missing_markers: List[int] = field(default_factory=list)
total_price: float = 0.0
currency: str = "USD"
class DifyTranslationError(Exception):
@@ -252,6 +256,11 @@ class DifyClient:
translated_text = data.get("answer", "")
usage = data.get("metadata", {}).get("usage", {})
# Extract price info from usage or metadata (may be string or number)
raw_price = usage.get("total_price", 0.0)
total_price = float(raw_price) if raw_price else 0.0
currency = usage.get("currency", "USD") or "USD"
self._total_tokens += usage.get("total_tokens", 0)
self._total_requests += 1
@@ -259,7 +268,9 @@ class DifyClient:
translated_text=translated_text,
total_tokens=usage.get("total_tokens", 0),
latency=usage.get("latency", 0.0),
conversation_id=data.get("conversation_id", "")
conversation_id=data.get("conversation_id", ""),
total_price=total_price,
currency=currency
)
def translate_batch(
@@ -297,6 +308,11 @@ class DifyClient:
answer = data.get("answer", "")
usage = data.get("metadata", {}).get("usage", {})
# Extract price info from usage or metadata (may be string or number)
raw_price = usage.get("total_price", 0.0)
total_price = float(raw_price) if raw_price else 0.0
currency = usage.get("currency", "USD") or "USD"
translations = self._parse_batch_response(answer, len(texts))
# Check for missing markers
@@ -314,7 +330,9 @@ class DifyClient:
total_tokens=usage.get("total_tokens", 0),
latency=usage.get("latency", 0.0),
conversation_id=data.get("conversation_id", ""),
missing_markers=missing_markers
missing_markers=missing_markers,
total_price=total_price,
currency=currency
)
def get_stats(self) -> dict:

View File

@@ -232,6 +232,8 @@ class TranslationService:
self._jobs_lock = threading.Lock()
self._total_tokens = 0
self._total_latency = 0.0
self._total_price = 0.0
self._currency = "USD"
def _load_raw_ocr_regions(
self,
@@ -459,6 +461,9 @@ class TranslationService:
self._total_tokens += response.total_tokens
self._total_latency += response.latency
self._total_price += response.total_price
if response.currency:
self._currency = response.currency
# Map translations back to items
translated_items = []
@@ -524,6 +529,9 @@ class TranslationService:
self._total_tokens += response.total_tokens
self._total_latency += response.latency
self._total_price += response.total_price
if response.currency:
self._currency = response.currency
return TranslatedItem(
element_id=item.element_id,
@@ -555,7 +563,9 @@ class TranslationService:
total_elements: int,
processing_time: float,
batch_count: int,
processing_track: str = 'direct'
processing_track: str = 'direct',
total_price: float = 0.0,
currency: str = 'USD'
) -> Dict:
"""
Build the translation result JSON structure.
@@ -613,6 +623,8 @@ class TranslationService:
'total_characters': total_chars,
'processing_time_seconds': round(processing_time, 2),
'total_tokens': self._total_tokens,
'total_price': round(total_price, 6),
'currency': currency,
'batch_count': batch_count
},
'translations': {}, # Empty for OCR Track
@@ -656,6 +668,8 @@ class TranslationService:
'total_characters': total_chars,
'processing_time_seconds': round(processing_time, 2),
'total_tokens': self._total_tokens,
'total_price': round(total_price, 6),
'currency': currency,
'batch_count': batch_count
},
'translations': translations
@@ -687,6 +701,8 @@ class TranslationService:
start_time = time.time()
self._total_tokens = 0
self._total_latency = 0.0
self._total_price = 0.0
self._currency = "USD"
logger.info(
f"Starting translation: task_id={task_id}, target={target_lang}"
@@ -752,7 +768,9 @@ class TranslationService:
total_elements=total_elements,
processing_time=processing_time,
batch_count=len(batches),
processing_track=processing_track
processing_track=processing_track,
total_price=self._total_price,
currency=self._currency
)
# Save result