Files
InnerOS/src/lib/engines/summary-generator.ts
T
jackyu66gitandClaude Opus 4.7 6530b20172 feat: guided light meditation + Edge TTS neural voice audio
- Add continuous guided light-body-scan meditation (16 segments, 97% coverage)
- Edge TTS API endpoint with Xiaoxiao neural voice at -40% rate
- Pre-generated chime WAVs and guidance MP3s in public/audio/
- Mute toggle in practice page, sequenced non-overlapping audio
- Fix middleware to serve /audio and /api/audio/tts without auth
- All LLM output localized to Chinese; fix blank home/settings pages
- DeepSeek API compat: json_object, increased max_tokens
- Analytics, cron reminders, onboarding flow fixes

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 20:08:29 +08:00

212 lines
6.7 KiB
TypeScript

import OpenAI from "openai";
import { prisma } from "@/lib/prisma";
import { keywordCheck, llmSafetyCheck, type SafetyResult } from "@/lib/safety";
import type { Lang } from "@/lib/i18n/types";
import { createServerT } from "@/lib/i18n/utils";
import zh from "../../../dictionaries/zh";
import en from "../../../dictionaries/en";
const DICTS = { zh, en } as const;
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface WeekData {
sessions: {
taskType: string;
taskDescription: string;
focusScore: number | null;
emotionScore: number | null;
notes: string | null;
durationSeconds: number;
createdAt: Date;
}[];
state: {
attentionLevel: number;
emotionStability: number;
stressLevel: number;
practiceStage: number;
consistencyScore: number;
streakDays: number;
totalSessionsCompleted: number;
};
}
interface GeneratedSummary {
summary: string;
safety: SafetyResult;
}
// ---------------------------------------------------------------------------
// LLM summary generation (LLM #3)
// ---------------------------------------------------------------------------
const SUMMARY_PROMPT = `You are a wise, warm inner development coach writing a weekly practice summary. Your tone is grounded, encouraging, and specific — never generic or saccharine.
You will receive data about the user's practice this week: sessions completed, task types, focus/emotion scores, reflections, and their current state metrics (attention, emotional stability, stress, consistency, streak, stage).
Write a 3-4 paragraph summary that:
1. Opens by acknowledging their commitment this week — be specific about what they practiced and how many sessions
2. Highlights patterns you notice: which types of practice led to better focus? Did their emotional awareness deepen? Were there difficult days they pushed through?
3. Notes changes in their state metrics from what you can infer — are they trending in a good direction?
4. Closes with gentle encouragement for the week ahead, grounded in what they've actually demonstrated (not empty motivation)
Voice rules:
- Be specific. Reference actual task types and patterns. "Your two attention training sessions showed stronger focus than your stress regulation work" is good. "Great job this week!" is not.
- Be honest. If it was a light week with only 1-2 sessions, acknowledge that without judgment.
- Never diagnose, never claim medical benefits, never use self-help clichés.
- Write in second person ("you").
- Keep it to 3-4 paragraphs, 200-300 words total.
IMPORTANT: The user interface is Chinese-only. Write the entire summary in Simplified Chinese (简体中文). Use "你" (not "you").`;
export async function generateWeeklySummary(
userId: string,
openai: OpenAI,
locale: Lang = "zh"
): Promise<GeneratedSummary> {
const t = createServerT(DICTS[locale]);
// Gather week data
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
const [sessions, userState] = await Promise.all([
prisma.session.findMany({
where: {
userId,
createdAt: { gte: weekAgo },
completed: true,
},
orderBy: { createdAt: "asc" },
select: {
taskType: true,
taskDescription: true,
focusScore: true,
emotionScore: true,
notes: true,
durationSeconds: true,
createdAt: true,
},
}),
prisma.userState.findUnique({ where: { userId } }),
]);
if (!userState || sessions.length === 0) {
return {
summary: t("engine.summary.no_sessions"),
safety: { safe: true },
};
}
const weekData: WeekData = {
sessions,
state: {
attentionLevel: userState.attentionLevel,
emotionStability: userState.emotionStability,
stressLevel: userState.stressLevel,
practiceStage: userState.practiceStage,
consistencyScore: userState.consistencyScore,
streakDays: userState.streakDays,
totalSessionsCompleted: userState.totalSessionsCompleted,
},
};
try {
const response = await openai.chat.completions.create({
model: "deepseek-v4-pro",
messages: [
{ role: "system", content: SUMMARY_PROMPT },
{
role: "user",
content: JSON.stringify(weekData),
},
],
temperature: 0.7,
max_tokens: 2000,
});
const summary = response.choices[0]?.message?.content?.trim();
if (!summary || summary.length < 50) throw new Error("Summary too short");
// Safety check
const keywordResult = keywordCheck(summary);
if (!keywordResult.safe) {
return { summary: t("engine.summary.safety_fallback"), safety: keywordResult };
}
const llmSafety = await llmSafetyCheck(summary, openai as any);
// Persist the summary (upsert — one per user per week)
const weekStart = new Date(weekAgo);
weekStart.setHours(0, 0, 0, 0);
const safetyFallback = t("engine.summary.safety_fallback");
await prisma.weeklySummary.upsert({
where: { userId },
create: {
userId,
summary: llmSafety.safe ? summary : safetyFallback,
weekStart,
},
update: {
summary: llmSafety.safe ? summary : safetyFallback,
weekStart,
},
});
return {
summary: llmSafety.safe ? summary : safetyFallback,
safety: llmSafety,
};
} catch {
const count = sessions.length;
const streak = userState.streakDays;
const fallback = t("engine.summary.fallback_template", {
count,
plural: count === 1 ? "" : "s",
streak,
streak_plural: streak === 1 ? "" : "s",
});
return { summary: fallback, safety: { safe: true } };
}
}
// ---------------------------------------------------------------------------
// Cron handler — generates summaries for all active users
// ---------------------------------------------------------------------------
export async function generateAllWeeklySummaries(
openai: OpenAI,
locale: Lang = "zh"
): Promise<{ userId: string; success: boolean }[]> {
// Find users who had sessions in the last 7 days
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
const activeUsers = await prisma.session.findMany({
where: {
createdAt: { gte: weekAgo },
completed: true,
},
select: { userId: true },
distinct: ["userId"],
});
const results: { userId: string; success: boolean }[] = [];
for (const { userId } of activeUsers) {
try {
await generateWeeklySummary(userId, openai, locale);
results.push({ userId, success: true });
} catch {
results.push({ userId, success: false });
}
}
return results;
}