Two features in one push (both backend, deploy together):
LYRA FOUNDER-STORY (per strategist Option C — mixed/medium-detail):
- COACH_CASUAL_SYSTEM_PROMPT: GRÜNDER-STORY sub-block
- Sharing-rules: ALWAYS on direct ask, RARELY proactive (only on
explicit isolation expressions "niemand versteht das"), NEVER in
SOS-mode, NEVER first-3-msgs, NEVER if user appears minor
- Detail-level: "aus persönlicher Erfahrung mit Spielsucht in seiner
Familie" — KEINE Namen, Verwandtschaftsgrade, Verlust-Details
- Post-share-pivot: "...aber jetzt zu dir: was ist gerade los?"
- COACH_SYSTEM_PROMPT (SOS): SOS-MODE LOCK — hard-Verbot Gründer-Story
zu erwähnen, auch bei direct-ask. Re-trigger-Risk zu hoch.
- DSGVO: brother bleibt komplett anonymisiert. Hans-Müller-DSB-review für
verbal-consent-doc empfohlen.
VOICE TIER-MAPPING (per user-decision: voice für ALLE tiers):
- New plan-features.voice config: provider + model + voiceId + dailyQuotaSeconds
- Tier-mapping:
- Free → Google TTS Neural2-F (de-DE), 60s/day, ~$4/1M chars
- Pro → Cartesia Sonic-2, 300s/day, ~$4/1M chars + ~75ms TTFT
- Legend → ElevenLabs Turbo v2.5, unlimited, ~$30/1M chars
- New backend/server/db/voiceQuota.ts:
- getRemainingVoiceQuota(userId, plan)
- consumeVoiceQuota(userId, seconds)
- estimateAudioSeconds(text)
- speak.post.ts komplett umgeschrieben als plan-aware dispatcher
- 14 tests passing (partial-consume, exhausted, day-rollover, edge-cases)
- Schema-migration 20260509_voice_quota:
ADD voice_seconds_used_today, voice_quota_reset_at to profiles
(auto-deploy via pipeline)
Pending Frontend (separate task):
- Voice-quota-UI in Settings/Profile (remaining seconds + upgrade-prompt
bei 429 quota_exceeded)
⚠️ Schema-migration auto-deploy via b38bf17 detection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
export type Plan = "free" | "pro" | "legend";
|
|
|
|
export type VoiceProvider = "elevenlabs" | "openai" | "google" | "cartesia" | "azure";
|
|
|
|
export interface VoiceConfig {
|
|
/** TTS-Provider für diesen Plan */
|
|
provider: VoiceProvider;
|
|
/** Provider-spezifische Model-ID (optional) */
|
|
model?: string;
|
|
/** Provider-spezifische Voice-ID (optional — fällt auf provider-default zurück) */
|
|
voiceId?: string;
|
|
/** Tages-Quota in Sekunden. 0 = unlimited */
|
|
dailyQuotaSeconds: number;
|
|
}
|
|
|
|
export interface PlanLimits {
|
|
/** Max. eigene Domains (Infinity = unbegrenzt) */
|
|
customDomains: number;
|
|
/** Freigeschaltete Domain-Slots füllen sich wieder auf (Community-Promotion) */
|
|
domainRefill: boolean;
|
|
/** Max. aktive Mail-Agenten (Infinity = unbegrenzt) */
|
|
mailAgents: number;
|
|
/** Erlaubte Scan-Intervalle in Stunden */
|
|
mailIntervalOptions: number[];
|
|
/** Zugang zur globalen HaGeZi-Blocklist (200k+) */
|
|
globalBlocklist: boolean;
|
|
/** Darf in der Community posten */
|
|
canPost: boolean;
|
|
/** Darf Gruppen gründen */
|
|
canCreateGroup: boolean;
|
|
/** Darf Domains direkt zur ReBreak Blocklist hinzufügen */
|
|
canAddToBlocklist: boolean;
|
|
/** Max. parallel registrierte Devices pro Account (Anti-Account-Sharing) */
|
|
maxDevices: number;
|
|
/** Primäres OpenRouter/Groq-Modell für KI-Coach */
|
|
aiModel: string;
|
|
/** Fallback-Modelle (werden der Reihe nach versucht wenn primary fehlschlägt) */
|
|
aiModelFallbacks: Array<{ provider: "groq" | "openrouter"; model: string }>;
|
|
/** AI-Provider: groq (Free/Pro) oder openrouter (Legend/Claude) */
|
|
aiProvider: "groq" | "openrouter";
|
|
/**
|
|
* Voice-Config: welcher TTS-Provider + Quota.
|
|
*
|
|
* Provider-Mapping (Cost-Reference 2026-05):
|
|
* Free → Google TTS Neural2 (~$4/1M chars, 60s/day cap)
|
|
* Pro → Cartesia Sonic-2 (~$4/1M chars, 300s/day cap, ~75ms first-byte)
|
|
* Legend → ElevenLabs Turbo v2.5 (~$30/1M chars, unlimited)
|
|
*/
|
|
voice: VoiceConfig;
|
|
}
|
|
|
|
export const PLAN_LIMITS: Record<Plan, PlanLimits> = {
|
|
free: {
|
|
customDomains: 5,
|
|
domainRefill: false,
|
|
mailAgents: 1,
|
|
mailIntervalOptions: [4],
|
|
globalBlocklist: false,
|
|
canPost: true,
|
|
canCreateGroup: false,
|
|
canAddToBlocklist: false,
|
|
maxDevices: 1,
|
|
aiModel: "llama-3.1-8b-instant",
|
|
aiModelFallbacks: [
|
|
{ provider: "groq", model: "llama-3.3-70b-versatile" },
|
|
{ provider: "groq", model: "gemma2-9b-it" },
|
|
{ provider: "openrouter", model: "meta-llama/llama-3.1-8b-instruct" },
|
|
],
|
|
aiProvider: "groq",
|
|
voice: {
|
|
provider: "google",
|
|
model: "de-DE-Neural2-F", // Google Cloud TTS Neural2 — natural, ~$4/1M chars
|
|
dailyQuotaSeconds: 60, // 1 Minute/Tag
|
|
},
|
|
},
|
|
pro: {
|
|
customDomains: 5,
|
|
domainRefill: true,
|
|
mailAgents: 3,
|
|
mailIntervalOptions: [1, 4, 8],
|
|
globalBlocklist: true,
|
|
canPost: true,
|
|
canCreateGroup: false,
|
|
canAddToBlocklist: false,
|
|
maxDevices: 1,
|
|
aiModel: "llama-3.3-70b-versatile",
|
|
aiModelFallbacks: [
|
|
{ provider: "groq", model: "llama-3.1-8b-instant" },
|
|
{ provider: "openrouter", model: "meta-llama/llama-3.3-70b-instruct" },
|
|
],
|
|
aiProvider: "groq",
|
|
voice: {
|
|
provider: "cartesia",
|
|
model: "sonic-2", // Cartesia Sonic-2 — ~75ms TTFT, native German, ~$4/1M chars
|
|
dailyQuotaSeconds: 300, // 5 Minuten/Tag
|
|
},
|
|
},
|
|
legend: {
|
|
customDomains: 10,
|
|
domainRefill: true,
|
|
mailAgents: Infinity,
|
|
mailIntervalOptions: [1, 4, 8],
|
|
globalBlocklist: true,
|
|
canPost: true,
|
|
canCreateGroup: true,
|
|
canAddToBlocklist: true,
|
|
maxDevices: 3,
|
|
aiModel: "anthropic/claude-3.5-haiku",
|
|
aiModelFallbacks: [
|
|
{ provider: "openrouter", model: "anthropic/claude-3-haiku" },
|
|
{ provider: "groq", model: "llama-3.3-70b-versatile" },
|
|
],
|
|
aiProvider: "openrouter",
|
|
voice: {
|
|
provider: "elevenlabs",
|
|
model: "eleven_turbo_v2_5", // ElevenLabs Turbo v2.5 — premium, ~$30/1M chars
|
|
dailyQuotaSeconds: 0, // 0 = unlimited
|
|
},
|
|
},
|
|
};
|
|
|
|
export function getPlanLimits(plan: string): PlanLimits {
|
|
// Legacy-Pläne auf neue Namen mappen
|
|
if (plan === "premium") return PLAN_LIMITS.legend;
|
|
if (plan === "standard") return PLAN_LIMITS.pro;
|
|
return PLAN_LIMITS[(plan as Plan) ?? "free"] ?? PLAN_LIMITS.free;
|
|
}
|