Fix preflight NameError, clean up debug logs, remove redundant voice button, fix Tooltip warning
This commit is contained in:
@@ -100,9 +100,5 @@ async def serve_voice_sample(
|
|||||||
|
|
||||||
media_type = _get_media_type(safe_filename)
|
media_type = _get_media_type(safe_filename)
|
||||||
file_size = file_path.stat().st_size
|
file_size = file_path.stat().st_size
|
||||||
first_bytes_hex = "N/A"
|
logger.warning(f"[Assets] Serving voice sample: {safe_filename} ({media_type}, {file_size} bytes)")
|
||||||
if file_size > 0:
|
|
||||||
with open(file_path, 'rb') as f:
|
|
||||||
first_bytes_hex = f.read(16).hex()
|
|
||||||
logger.warning(f"[Assets] Serving voice sample: {safe_filename} ({media_type}, {file_size} bytes, first_16hex: {first_bytes_hex})")
|
|
||||||
return FileResponse(file_path, media_type=media_type)
|
return FileResponse(file_path, media_type=media_type)
|
||||||
@@ -541,44 +541,30 @@ async def create_voice_clone(
|
|||||||
preview_mime_type = "audio/wav"
|
preview_mime_type = "audio/wav"
|
||||||
actual_filename = None # Default if preview save fails
|
actual_filename = None # Default if preview save fails
|
||||||
|
|
||||||
logger.warning(f"[VoiceClone] qwen3 result preview_audio_bytes type: {type(preview_audio_bytes)}, length: {len(preview_audio_bytes) if preview_audio_bytes else 0}")
|
|
||||||
|
|
||||||
if preview_audio_bytes and len(preview_audio_bytes) > 0:
|
if preview_audio_bytes and len(preview_audio_bytes) > 0:
|
||||||
from utils.media_utils import detect_audio_format, ensure_audio_extension
|
from utils.media_utils import detect_audio_format, ensure_audio_extension
|
||||||
|
|
||||||
# Log first few bytes for debugging
|
|
||||||
first_bytes = preview_audio_bytes[:16].hex()
|
|
||||||
logger.warning(f"[VoiceClone] Preview audio first 16 bytes (hex): {first_bytes}")
|
|
||||||
|
|
||||||
detected_fmt, preview_mime_type = detect_audio_format(preview_audio_bytes)
|
detected_fmt, preview_mime_type = detect_audio_format(preview_audio_bytes)
|
||||||
logger.warning(f"[VoiceClone] Detected preview audio format: {detected_fmt} ({preview_mime_type}), {len(preview_audio_bytes)} bytes")
|
logger.warning(f"[VoiceClone] Detected preview audio format: {detected_fmt} ({preview_mime_type}), {len(preview_audio_bytes)} bytes")
|
||||||
|
|
||||||
# Build filename with correct extension based on actual content format
|
# Build filename with correct extension based on actual content format
|
||||||
original_stem = Path(filename).stem
|
original_stem = Path(filename).stem
|
||||||
preview_filename = f"preview_{original_stem}"
|
preview_filename = f"preview_{original_stem}"
|
||||||
logger.warning(f"[VoiceClone] Original filename stem: {original_stem}, preview before: {preview_filename}")
|
|
||||||
|
|
||||||
preview_filename = ensure_audio_extension(preview_filename, preview_audio_bytes)
|
preview_filename = ensure_audio_extension(preview_filename, preview_audio_bytes)
|
||||||
logger.warning(f"[VoiceClone] Preview filename (corrected ext): {preview_filename}")
|
|
||||||
|
|
||||||
user_voice_dir = get_user_workspace(user_id) / "assets" / "voice_samples"
|
user_voice_dir = get_user_workspace(user_id) / "assets" / "voice_samples"
|
||||||
logger.warning(f"[VoiceClone] user_id: {user_id}")
|
|
||||||
logger.warning(f"[VoiceClone] user_voice_dir: {user_voice_dir}")
|
|
||||||
logger.warning(f"[VoiceClone] directory exists: {user_voice_dir.exists()}")
|
|
||||||
saved_preview_path, error = save_file_safely(preview_audio_bytes, user_voice_dir, preview_filename)
|
saved_preview_path, error = save_file_safely(preview_audio_bytes, user_voice_dir, preview_filename)
|
||||||
|
|
||||||
if not error and saved_preview_path:
|
if not error and saved_preview_path:
|
||||||
# Use actual saved filename (may have UUID suffix added by save_file_safely)
|
# Use actual saved filename (may have UUID suffix added by save_file_safely)
|
||||||
actual_filename = saved_preview_path.name
|
actual_filename = saved_preview_path.name
|
||||||
preview_url = f"/api/assets/{user_id}/voice_samples/{actual_filename}"
|
preview_url = f"/api/assets/{user_id}/voice_samples/{actual_filename}"
|
||||||
logger.warning(f"[VoiceClone] Saved preview audio to: {saved_preview_path}")
|
logger.warning(f"[VoiceClone] Saved preview: {actual_filename} ({saved_preview_path.stat().st_size} bytes, {preview_mime_type})")
|
||||||
|
|
||||||
# Verify file exists
|
# Verify file exists
|
||||||
if not saved_preview_path.exists():
|
if not saved_preview_path.exists():
|
||||||
logger.warning(f"[VoiceClone] Preview file does not exist after save: {saved_preview_path}")
|
logger.warning(f"[VoiceClone] Preview file does not exist after save: {saved_preview_path}")
|
||||||
preview_url = None
|
preview_url = None
|
||||||
else:
|
|
||||||
logger.warning(f"[VoiceClone] Preview file size: {saved_preview_path.stat().st_size} bytes")
|
|
||||||
else:
|
else:
|
||||||
logger.warning(f"[VoiceClone] Failed to save preview audio: {error}")
|
logger.warning(f"[VoiceClone] Failed to save preview audio: {error}")
|
||||||
|
|
||||||
@@ -586,7 +572,6 @@ async def create_voice_clone(
|
|||||||
# Use the preview file (with corrected .wav extension) as the main asset file
|
# Use the preview file (with corrected .wav extension) as the main asset file
|
||||||
has_valid_preview = preview_audio_bytes and len(preview_audio_bytes) > 0 and saved_preview_path
|
has_valid_preview = preview_audio_bytes and len(preview_audio_bytes) > 0 and saved_preview_path
|
||||||
stored_filename = actual_filename if has_valid_preview else filename
|
stored_filename = actual_filename if has_valid_preview else filename
|
||||||
logger.warning(f"[VoiceClone] stored_filename: {stored_filename}, has_valid_preview: {has_valid_preview}")
|
|
||||||
asset_id = save_asset_to_library(
|
asset_id = save_asset_to_library(
|
||||||
db=db,
|
db=db,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@@ -606,9 +591,6 @@ async def create_voice_clone(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.warning(f"[VoiceClone] Response preview_url: {preview_url}")
|
|
||||||
logger.warning(f"[VoiceClone] Response stored_filename: {stored_filename}")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"custom_voice_id": custom_voice_id,
|
"custom_voice_id": custom_voice_id,
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ if TYPE_CHECKING:
|
|||||||
from .pricing_service import PricingService
|
from .pricing_service import PricingService
|
||||||
|
|
||||||
|
|
||||||
|
def _should_enforce_limit(limit_value: int, tier: str) -> bool:
|
||||||
|
"""
|
||||||
|
Determine if a limit should be enforced.
|
||||||
|
- Free tier: 0 means DISABLED (not unlimited)
|
||||||
|
- Basic/Pro/Enterprise: 0 means UNLIMITED
|
||||||
|
"""
|
||||||
|
return limit_value > 0
|
||||||
|
|
||||||
|
|
||||||
class LimitValidator:
|
class LimitValidator:
|
||||||
"""Validates subscription limits for API usage."""
|
"""Validates subscription limits for API usage."""
|
||||||
|
|
||||||
@@ -107,20 +116,6 @@ class LimitValidator:
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Helper: Check if a limit should be enforced based on tier
|
|
||||||
def should_enforce_limit(limit_value: int, tier: str) -> bool:
|
|
||||||
"""
|
|
||||||
Determine if a limit should be enforced.
|
|
||||||
- Free tier: 0 means DISABLED (not unlimited)
|
|
||||||
- Basic/Pro/Enterprise: 0 means UNLIMITED
|
|
||||||
"""
|
|
||||||
if tier == 'free':
|
|
||||||
# Free tier: 0 means disabled
|
|
||||||
return limit_value > 0
|
|
||||||
else:
|
|
||||||
# Basic/Pro/Enterprise: 0 means unlimited
|
|
||||||
return limit_value > 0
|
|
||||||
|
|
||||||
# Get user limits with error handling (STRICT: fail on errors)
|
# Get user limits with error handling (STRICT: fail on errors)
|
||||||
# CRITICAL: Expire SQLAlchemy objects to ensure we get fresh plan data after renewal
|
# CRITICAL: Expire SQLAlchemy objects to ensure we get fresh plan data after renewal
|
||||||
try:
|
try:
|
||||||
@@ -263,7 +258,7 @@ class LimitValidator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||||
if should_enforce_limit(ai_text_gen_limit, user_tier) and current_total_llm_calls >= ai_text_gen_limit:
|
if _should_enforce_limit(ai_text_gen_limit, user_tier) and current_total_llm_calls >= ai_text_gen_limit:
|
||||||
logger.error(f"[Subscription Check] AI text generation call limit exceeded for user {user_id}: {current_total_llm_calls}/{ai_text_gen_limit} (provider: {display_provider_name})")
|
logger.error(f"[Subscription Check] AI text generation call limit exceeded for user {user_id}: {current_total_llm_calls}/{ai_text_gen_limit} (provider: {display_provider_name})")
|
||||||
result = (False, f"AI text generation call limit reached. Used {current_total_llm_calls} of {ai_text_gen_limit} total AI text generation calls this billing period.", {
|
result = (False, f"AI text generation call limit reached. Used {current_total_llm_calls} of {ai_text_gen_limit} total AI text generation calls this billing period.", {
|
||||||
'current_calls': current_total_llm_calls,
|
'current_calls': current_total_llm_calls,
|
||||||
@@ -296,7 +291,7 @@ class LimitValidator:
|
|||||||
call_limit = limits['limits'].get(f"{provider_name}_calls", 0) or 0
|
call_limit = limits['limits'].get(f"{provider_name}_calls", 0) or 0
|
||||||
|
|
||||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||||
if should_enforce_limit(call_limit, user_tier) and current_calls >= call_limit:
|
if _should_enforce_limit(call_limit, user_tier) and current_calls >= call_limit:
|
||||||
logger.error(f"[Subscription Check] Call limit exceeded for user {user_id}, provider {display_provider_name}: {current_calls}/{call_limit}")
|
logger.error(f"[Subscription Check] Call limit exceeded for user {user_id}, provider {display_provider_name}: {current_calls}/{call_limit}")
|
||||||
result = (False, f"API call limit reached for {display_provider_name}. Used {current_calls} of {call_limit} calls this billing period.", {
|
result = (False, f"API call limit reached for {display_provider_name}. Used {current_calls} of {call_limit} calls this billing period.", {
|
||||||
'current_calls': current_calls,
|
'current_calls': current_calls,
|
||||||
@@ -329,7 +324,7 @@ class LimitValidator:
|
|||||||
token_limit = limits['limits'].get(f"{provider_name}_tokens", 0) or 0
|
token_limit = limits['limits'].get(f"{provider_name}_tokens", 0) or 0
|
||||||
|
|
||||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||||
if should_enforce_limit(token_limit, user_tier) and (current_tokens + tokens_requested) > token_limit:
|
if _should_enforce_limit(token_limit, user_tier) and (current_tokens + tokens_requested) > token_limit:
|
||||||
result = (False, f"Token limit would be exceeded for {display_provider_name}. Current: {current_tokens}, Requested: {tokens_requested}, Limit: {token_limit}", {
|
result = (False, f"Token limit would be exceeded for {display_provider_name}. Current: {current_tokens}, Requested: {tokens_requested}, Limit: {token_limit}", {
|
||||||
'current_tokens': current_tokens,
|
'current_tokens': current_tokens,
|
||||||
'requested_tokens': tokens_requested,
|
'requested_tokens': tokens_requested,
|
||||||
@@ -363,7 +358,7 @@ class LimitValidator:
|
|||||||
try:
|
try:
|
||||||
cost_limit = limits['limits'].get('monthly_cost', 0) or 0
|
cost_limit = limits['limits'].get('monthly_cost', 0) or 0
|
||||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||||
if should_enforce_limit(cost_limit, user_tier) and usage.total_cost >= cost_limit:
|
if _should_enforce_limit(cost_limit, user_tier) and usage.total_cost >= cost_limit:
|
||||||
result = (False, f"Monthly cost limit reached. Current cost: ${usage.total_cost:.2f}, Limit: ${cost_limit:.2f}", {
|
result = (False, f"Monthly cost limit reached. Current cost: ${usage.total_cost:.2f}, Limit: ${cost_limit:.2f}", {
|
||||||
'current_cost': usage.total_cost,
|
'current_cost': usage.total_cost,
|
||||||
'limit': cost_limit,
|
'limit': cost_limit,
|
||||||
@@ -583,7 +578,7 @@ class LimitValidator:
|
|||||||
projected_total_llm_calls = total_llm_calls + 1
|
projected_total_llm_calls = total_llm_calls + 1
|
||||||
|
|
||||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||||
if should_enforce_limit(ai_text_gen_limit, tier) and projected_total_llm_calls > ai_text_gen_limit:
|
if _should_enforce_limit(ai_text_gen_limit, tier) and projected_total_llm_calls > ai_text_gen_limit:
|
||||||
error_info = {
|
error_info = {
|
||||||
'current_calls': total_llm_calls,
|
'current_calls': total_llm_calls,
|
||||||
'limit': ai_text_gen_limit,
|
'limit': ai_text_gen_limit,
|
||||||
@@ -691,7 +686,7 @@ class LimitValidator:
|
|||||||
token_limit = limits.get(provider_tokens_key, 0) or 0
|
token_limit = limits.get(provider_tokens_key, 0) or 0
|
||||||
|
|
||||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||||
if should_enforce_limit(token_limit, tier) and tokens_requested > 0:
|
if _should_enforce_limit(token_limit, tier) and tokens_requested > 0:
|
||||||
projected_tokens = current_provider_tokens + tokens_requested
|
projected_tokens = current_provider_tokens + tokens_requested
|
||||||
logger.info(f" └─ Token Check: {current_provider_tokens} (current) + {tokens_requested} (requested) = {projected_tokens} (total) / {token_limit} (limit)")
|
logger.info(f" └─ Token Check: {current_provider_tokens} (current) + {tokens_requested} (requested) = {projected_tokens} (total) / {token_limit} (limit)")
|
||||||
|
|
||||||
@@ -754,7 +749,7 @@ class LimitValidator:
|
|||||||
projected_images = total_images + 1
|
projected_images = total_images + 1
|
||||||
|
|
||||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||||
if should_enforce_limit(image_limit, tier) and projected_images > image_limit:
|
if _should_enforce_limit(image_limit, tier) and projected_images > image_limit:
|
||||||
error_info = {
|
error_info = {
|
||||||
'current_images': total_images,
|
'current_images': total_images,
|
||||||
'limit': image_limit,
|
'limit': image_limit,
|
||||||
@@ -776,7 +771,7 @@ class LimitValidator:
|
|||||||
projected_video_calls = total_video_calls + 1
|
projected_video_calls = total_video_calls + 1
|
||||||
|
|
||||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||||
if should_enforce_limit(video_limit, tier) and projected_video_calls > video_limit:
|
if _should_enforce_limit(video_limit, tier) and projected_video_calls > video_limit:
|
||||||
error_info = {
|
error_info = {
|
||||||
'current_calls': total_video_calls,
|
'current_calls': total_video_calls,
|
||||||
'limit': video_limit,
|
'limit': video_limit,
|
||||||
@@ -796,7 +791,7 @@ class LimitValidator:
|
|||||||
projected_image_edit_calls = total_image_edit_calls + 1
|
projected_image_edit_calls = total_image_edit_calls + 1
|
||||||
|
|
||||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||||
if should_enforce_limit(image_edit_limit, tier) and projected_image_edit_calls > image_edit_limit:
|
if _should_enforce_limit(image_edit_limit, tier) and projected_image_edit_calls > image_edit_limit:
|
||||||
error_info = {
|
error_info = {
|
||||||
'current_calls': total_image_edit_calls,
|
'current_calls': total_image_edit_calls,
|
||||||
'limit': image_edit_limit,
|
'limit': image_edit_limit,
|
||||||
@@ -833,7 +828,7 @@ class LimitValidator:
|
|||||||
# Check WaveSpeed combined limit if actual_provider is WaveSpeed
|
# Check WaveSpeed combined limit if actual_provider is WaveSpeed
|
||||||
if actual_provider_name == 'wavespeed':
|
if actual_provider_name == 'wavespeed':
|
||||||
wavespeed_limit = limits.get('wavespeed_calls', 0) or 0
|
wavespeed_limit = limits.get('wavespeed_calls', 0) or 0
|
||||||
if should_enforce_limit(wavespeed_limit, tier):
|
if _should_enforce_limit(wavespeed_limit, tier):
|
||||||
wavespeed_usage = usage.wavespeed_calls or 0
|
wavespeed_usage = usage.wavespeed_calls or 0
|
||||||
projected_wavespeed = wavespeed_usage + 1
|
projected_wavespeed = wavespeed_usage + 1
|
||||||
if projected_wavespeed > wavespeed_limit:
|
if projected_wavespeed > wavespeed_limit:
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ class UsageTrackingService:
|
|||||||
self._enforce_cache: Dict[str, Dict[str, Any]] = {}
|
self._enforce_cache: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
def _get_authoritative_billing_period_keys(self, user_id: str, billing_period: Optional[str] = None) -> Dict[str, Any]:
|
def _get_authoritative_billing_period_keys(self, user_id: str, billing_period: Optional[str] = None) -> Dict[str, Any]:
|
||||||
"""Return authoritative billing period lookup keys anchored to subscription period boundaries."""
|
"""Return authoritative billing period lookup keys. Always uses calendar month for consistency."""
|
||||||
subscription = self.db.query(UserSubscription).filter(
|
subscription = self.db.query(UserSubscription).filter(
|
||||||
UserSubscription.user_id == user_id
|
UserSubscription.user_id == user_id
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
# If caller explicitly requested a billing period, keep it authoritative for that read.
|
# If caller explicitly requested a billing period, use it
|
||||||
if billing_period:
|
if billing_period:
|
||||||
return {
|
return {
|
||||||
"billing_period": billing_period,
|
"billing_period": billing_period,
|
||||||
@@ -59,23 +59,15 @@ class UsageTrackingService:
|
|||||||
"period_end": subscription.current_period_end if subscription else None,
|
"period_end": subscription.current_period_end if subscription else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if subscription and subscription.current_period_start and subscription.current_period_end:
|
# ALWAYS use current calendar month for billing period to ensure consistency
|
||||||
start_key = subscription.current_period_start.strftime("%Y-%m")
|
# This prevents data loss when subscription spans month boundaries
|
||||||
end_key = subscription.current_period_end.strftime("%Y-%m")
|
current_period = datetime.now().strftime("%Y-%m")
|
||||||
lookup_periods = [start_key] if start_key == end_key else [start_key, end_key]
|
|
||||||
return {
|
|
||||||
"billing_period": start_key,
|
|
||||||
"lookup_periods": lookup_periods,
|
|
||||||
"period_start": subscription.current_period_start,
|
|
||||||
"period_end": subscription.current_period_end,
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved_period = self.pricing_service.get_current_billing_period(user_id) or datetime.now().strftime("%Y-%m")
|
|
||||||
return {
|
return {
|
||||||
"billing_period": resolved_period,
|
"billing_period": current_period,
|
||||||
"lookup_periods": [resolved_period],
|
"lookup_periods": [current_period],
|
||||||
"period_start": None,
|
"period_start": subscription.current_period_start if subscription else None,
|
||||||
"period_end": None,
|
"period_end": subscription.current_period_end if subscription else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def track_api_usage(self, user_id: str, provider: APIProvider,
|
async def track_api_usage(self, user_id: str, provider: APIProvider,
|
||||||
@@ -207,11 +199,14 @@ class UsageTrackingService:
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
if not summary:
|
if not summary:
|
||||||
|
logger.info(f"[UsageTracking] Creating new UsageSummary for user={user_id}, period={period_keys['billing_period']}")
|
||||||
summary = UsageSummary(
|
summary = UsageSummary(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
billing_period=period_keys["billing_period"]
|
billing_period=period_keys["billing_period"]
|
||||||
)
|
)
|
||||||
self.db.add(summary)
|
self.db.add(summary)
|
||||||
|
else:
|
||||||
|
logger.debug(f"[UsageTracking] Found existing UsageSummary for user={user_id}, period={summary.billing_period}, calls={summary.total_calls}")
|
||||||
|
|
||||||
# Update provider-specific counters
|
# Update provider-specific counters
|
||||||
provider_name = provider.value
|
provider_name = provider.value
|
||||||
@@ -384,12 +379,19 @@ class UsageTrackingService:
|
|||||||
period_keys = self._get_authoritative_billing_period_keys(user_id, requested_billing_period)
|
period_keys = self._get_authoritative_billing_period_keys(user_id, requested_billing_period)
|
||||||
billing_period = period_keys["billing_period"]
|
billing_period = period_keys["billing_period"]
|
||||||
|
|
||||||
|
logger.debug(f"[get_user_usage_stats] user={user_id}, billing_period={billing_period}, lookup_periods={period_keys['lookup_periods']}")
|
||||||
|
|
||||||
# Get usage summary
|
# Get usage summary
|
||||||
summary = self.db.query(UsageSummary).filter(
|
summary = self.db.query(UsageSummary).filter(
|
||||||
UsageSummary.user_id == user_id,
|
UsageSummary.user_id == user_id,
|
||||||
UsageSummary.billing_period.in_(period_keys["lookup_periods"])
|
UsageSummary.billing_period.in_(period_keys["lookup_periods"])
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
if summary:
|
||||||
|
logger.debug(f"[get_user_usage_stats] Found summary: period={summary.billing_period}, calls={summary.total_calls}, cost={summary.total_cost}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"[get_user_usage_stats] No summary found for user={user_id}, period={billing_period}")
|
||||||
|
|
||||||
# Get user limits
|
# Get user limits
|
||||||
limits = self.pricing_service.get_user_limits(user_id)
|
limits = self.pricing_service.get_user_limits(user_id)
|
||||||
|
|
||||||
|
|||||||
@@ -482,7 +482,7 @@ class SpeechGenerator:
|
|||||||
|
|
||||||
audio_url = self._extract_audio_url(outputs)
|
audio_url = self._extract_audio_url(outputs)
|
||||||
downloaded_audio = self._download_audio(audio_url, timeout)
|
downloaded_audio = self._download_audio(audio_url, timeout)
|
||||||
logger.warning(f"[WaveSpeed] qwen3_voice_clone downloaded {len(downloaded_audio)} bytes, first_16hex: {downloaded_audio[:16].hex()}")
|
logger.warning(f"[WaveSpeed] qwen3_voice_clone downloaded {len(downloaded_audio)} bytes")
|
||||||
return downloaded_audio
|
return downloaded_audio
|
||||||
|
|
||||||
def cosyvoice_voice_clone(
|
def cosyvoice_voice_clone(
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const PrimaryButton = React.forwardRef<HTMLButtonElement, PrimaryButtonPr
|
|||||||
|
|
||||||
return tooltip ? (
|
return tooltip ? (
|
||||||
<Tooltip title={tooltip} arrow>
|
<Tooltip title={tooltip} arrow>
|
||||||
{button}
|
<span style={{ display: "inline-flex" }}>{button}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
button
|
button
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ import {
|
|||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Collapse,
|
Collapse,
|
||||||
FormControlLabel,
|
|
||||||
Checkbox,
|
|
||||||
Divider,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -153,7 +150,6 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
|
|||||||
const [playingPreview, setPlayingPreview] = useState<string | null>(null);
|
const [playingPreview, setPlayingPreview] = useState<string | null>(null);
|
||||||
const [showVoiceClonePanel, setShowVoiceClonePanel] = useState(false);
|
const [showVoiceClonePanel, setShowVoiceClonePanel] = useState(false);
|
||||||
const [voiceCreated, setVoiceCreated] = useState(false);
|
const [voiceCreated, setVoiceCreated] = useState(false);
|
||||||
const [useCreatedVoice, setUseCreatedVoice] = useState(true);
|
|
||||||
const [redoingClone, setRedoingClone] = useState(false);
|
const [redoingClone, setRedoingClone] = useState(false);
|
||||||
const [selectOpen, setSelectOpen] = useState(false);
|
const [selectOpen, setSelectOpen] = useState(false);
|
||||||
const [tuneModalOpen, setTuneModalOpen] = useState(false);
|
const [tuneModalOpen, setTuneModalOpen] = useState(false);
|
||||||
@@ -326,7 +322,6 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
|
|||||||
|
|
||||||
const handleVoiceSet = useCallback(() => {
|
const handleVoiceSet = useCallback(() => {
|
||||||
setVoiceCreated(true);
|
setVoiceCreated(true);
|
||||||
setUseCreatedVoice(true);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleRedoClone = useCallback(() => {
|
const handleRedoClone = useCallback(() => {
|
||||||
@@ -335,18 +330,15 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
|
|||||||
setRedoingClone(true);
|
setRedoingClone(true);
|
||||||
setShowVoiceClonePanel(true);
|
setShowVoiceClonePanel(true);
|
||||||
setVoiceCreated(false);
|
setVoiceCreated(false);
|
||||||
setUseCreatedVoice(true);
|
|
||||||
}, 150);
|
}, 150);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDoneWithVoice = useCallback(() => {
|
const handleDoneWithVoice = useCallback(() => {
|
||||||
if (useCreatedVoice) {
|
fetchVoiceClone();
|
||||||
fetchVoiceClone();
|
|
||||||
}
|
|
||||||
setShowVoiceClonePanel(false);
|
setShowVoiceClonePanel(false);
|
||||||
setVoiceCreated(false);
|
setVoiceCreated(false);
|
||||||
setRedoingClone(false);
|
setRedoingClone(false);
|
||||||
}, [useCreatedVoice]);
|
}, []);
|
||||||
|
|
||||||
const handleCancelRedo = useCallback(() => {
|
const handleCancelRedo = useCallback(() => {
|
||||||
setShowVoiceClonePanel(false);
|
setShowVoiceClonePanel(false);
|
||||||
@@ -362,7 +354,6 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
setShowVoiceClonePanel(true);
|
setShowVoiceClonePanel(true);
|
||||||
setVoiceCreated(false);
|
setVoiceCreated(false);
|
||||||
setUseCreatedVoice(true);
|
|
||||||
setRedoingClone(false);
|
setRedoingClone(false);
|
||||||
}
|
}
|
||||||
}, [showVoiceClonePanel]);
|
}, [showVoiceClonePanel]);
|
||||||
@@ -1056,7 +1047,7 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
|
|||||||
border: "1px solid rgba(16, 185, 129, 0.3)",
|
border: "1px solid rgba(16, 185, 129, 0.3)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1.5 }}>
|
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
|
||||||
<CheckCircle sx={{ color: "#10b981", fontSize: "1.25rem" }} />
|
<CheckCircle sx={{ color: "#10b981", fontSize: "1.25rem" }} />
|
||||||
<Typography variant="subtitle2" sx={{ color: "#10b981", fontWeight: 700 }}>
|
<Typography variant="subtitle2" sx={{ color: "#10b981", fontWeight: 700 }}>
|
||||||
{redoingClone ? "Voice Clone Updated!" : "Voice Clone Created Successfully!"}
|
{redoingClone ? "Voice Clone Updated!" : "Voice Clone Created Successfully!"}
|
||||||
@@ -1064,29 +1055,9 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Typography variant="body2" sx={{ color: "#475569", mb: 1.5, fontSize: "0.875rem" }}>
|
<Typography variant="body2" sx={{ color: "#475569", mb: 1.5, fontSize: "0.875rem" }}>
|
||||||
{redoingClone ? "Your voice clone has been updated." : "Your custom voice clone is ready. Would you like to use this voice for your podcast?"}
|
{redoingClone ? "Your voice clone has been updated and will be used for your podcast." : "Your custom voice clone is ready and will be used for your podcast."}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={useCreatedVoice}
|
|
||||||
onChange={(e) => setUseCreatedVoice(e.target.checked)}
|
|
||||||
sx={{
|
|
||||||
color: "#10b981",
|
|
||||||
"&.Mui-checked": { color: "#10b981" },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={
|
|
||||||
<Typography sx={{ color: "#1e293b", fontWeight: 500, fontSize: "0.9375rem" }}>
|
|
||||||
Use this voice for my podcast
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Divider sx={{ my: 2, borderColor: "rgba(0,0,0,0.08)" }} />
|
|
||||||
|
|
||||||
<Stack direction="row" spacing={1.5} justifyContent="flex-end">
|
<Stack direction="row" spacing={1.5} justifyContent="flex-end">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCancelRedo}
|
onClick={handleCancelRedo}
|
||||||
@@ -1101,24 +1072,18 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleDoneWithVoice}
|
onClick={handleDoneWithVoice}
|
||||||
sx={{
|
sx={{
|
||||||
background: useCreatedVoice
|
background: "linear-gradient(135deg, #10b981 0%, #059669 100%)",
|
||||||
? "linear-gradient(135deg, #10b981 0%, #059669 100%)"
|
|
||||||
: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
px: 3,
|
px: 3,
|
||||||
boxShadow: useCreatedVoice
|
boxShadow: "0 4px 12px rgba(16, 185, 129, 0.3)",
|
||||||
? "0 4px 12px rgba(16, 185, 129, 0.3)"
|
|
||||||
: "0 4px 12px rgba(102, 126, 234, 0.3)",
|
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
background: useCreatedVoice
|
background: "linear-gradient(135deg, #059669 0%, #047857 100%)",
|
||||||
? "linear-gradient(135deg, #059669 0%, #047857 100%)"
|
|
||||||
: "linear-gradient(135deg, #764ba2 0%, #667eea 100%)",
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{useCreatedVoice ? "Use This Voice" : "Done"}
|
Done
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user