Add subreddit tone profiles for message style guidance

Introduces a tone-profile system that lets the processor adapt outgoing
message style per-subreddit (and per-flair) instead of using one tone
for everything. Profiles act as a "tone mic" layered after the base
prompt and strategy override, so they shape voice without redefining
the whole message.

- src/config/tone-profiles.ts defines the catalog (default, dominant,
  submissive, playful, direct, ...) and a resolveTone() helper that
  falls back to the default profile for unknown keys.
- config.yaml maps subreddits to tone keys; the schema in
  src/config/index.ts validates it.
- src/services/processor.ts threads the resolved tone into the prompt
  build, picking based on subreddit and optional link flair.
- src/reddit/api.ts adds link_flair_text to RedditPost so the
  processor can use it for tone selection.
- src/ai/openrouter.ts accepts and forwards the tone-shaped prompt.
- public/index.html surfaces the active tone in the dashboard.
- Tests cover the catalog, config validation, and processor integration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jjbruntonandClaude Opus 4.7 committed 2026-05-22 22:29:01 +01:00
1 parent 0c7df6e1d3
commit 46a213e3e0
10 files changed
+497 -51

No files matched your search

+21 -4
View File
@@ -226,6 +226,8 @@ ${this.config.openrouter.imageAnalysisPrompt}`;
successfulExamples: string[] = [],
strategyPrompt?: string,
attractivenessScore: number | null = null,
postFlair?: string | null,
toneGuidance?: string,
): Promise<string> {
try {
this.logger.debug('Generating message...');
@@ -234,13 +236,19 @@ ${this.config.openrouter.imageAnalysisPrompt}`;
.replace('{analysis}', imageAnalysis)
.replace('{title}', postTitle)
.replace('{author}', authorName)
.replace('{rating}', attractivenessScore != null ? String(attractivenessScore) : 'unknown');
.replace('{rating}', attractivenessScore != null ? String(attractivenessScore) : 'unknown')
.replace('{flair}', postFlair || '(no flair)');
// Append strategy-specific style guidance if provided
if (strategyPrompt) {
prompt += `\n\n${strategyPrompt}`;
}
// Append tone guidance if provided
if (toneGuidance) {
prompt += `\n\n${toneGuidance}`;
}
// Add successful examples if available
if (successfulExamples.length > 0) {
const examplesText = successfulExamples
@@ -371,6 +379,8 @@ Just write the message, nothing else.`;
detectedGender: DetectedGender,
successfulExamples: string[] = [],
strategyPrompt?: string,
postFlair?: string | null,
toneGuidance?: string,
): Promise<string> {
try {
this.logger.debug('Generating chat opener...');
@@ -379,12 +389,17 @@ Just write the message, nothing else.`;
.replace('{title}', postTitle)
.replace('{selftext}', selftext || '(no body)')
.replace('{author}', authorName)
.replace('{gender}', detectedGender);
.replace('{gender}', detectedGender)
.replace('{flair}', postFlair || '(no flair)');
if (strategyPrompt) {
prompt += `\n\n${strategyPrompt}`;
}
if (toneGuidance) {
prompt += `\n\n${toneGuidance}`;
}
if (successfulExamples.length > 0) {
const examplesText = successfulExamples
.map((ex, i) => `Example ${i + 1}:\n${ex}`)
@@ -482,10 +497,12 @@ Just write the message, nothing else.`;
postTitle: string,
authorName: string,
successfulExamples: string[] = [],
strategyPrompt?: string
strategyPrompt?: string,
postFlair?: string | null,
toneGuidance?: string,
): Promise<{ analysis: ImageAnalysisResult; message: string }> {
const analysis = await this.analyzeImage(imageBase64, mimeType);
const message = await this.generateMessage(analysis.description, postTitle, authorName, successfulExamples, strategyPrompt, analysis.attractivenessScore);
const message = await this.generateMessage(analysis.description, postTitle, authorName, successfulExamples, strategyPrompt, analysis.attractivenessScore, postFlair, toneGuidance);
return { analysis, message };
}
}