chahinebrini b40b8465b9 feat(lyra,voice): founder-story + voice-tier-mapping + quota system
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>
2026-05-09 16:28:36 +02:00

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;
}