Update source code and remove runtime data from tracking

Add gitignore rules for browser session data, SQLite databases,
media downloads, and build artifacts. Remove previously tracked
runtime files from the repository.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jjbruntonandClaude Opus 4.6 committed 2026-05-20 18:54:07 +01:00
1 parent e90213d1ff
commit dfa0580859
23 files changed
+5324 -347

No files matched your search

+178 -44
View File
@@ -65,64 +65,131 @@ export class OpenRouterClient {
}
async analyzeImage(imageBase64: string, mimeType: string): Promise<ImageAnalysisResult> {
try {
this.logger.debug('Analyzing image with AI...');
const models = [
this.config.openrouter.imageAnalysisModel,
...this.config.openrouter.imageAnalysisFallbackModels,
];
const genderPrompt = `First, identify the gender of the person in this image. Respond with EXACTLY one of these on the first line: GENDER:male, GENDER:female, GENDER:other, or GENDER:unknown
for (let i = 0; i < models.length; i++) {
const model = models[i];
const isFallback = i > 0;
try {
if (isFallback) {
this.logger.info(`Trying fallback model: ${model}`);
} else {
this.logger.debug('Analyzing image with AI...');
}
const result = await this.tryAnalyzeWithModel(model, imageBase64, mimeType);
if (result) {
if (isFallback) {
this.logger.info(`Fallback model ${model} succeeded`);
}
return result;
}
// Result was null = refusal, try next model
this.logger.warn(`Model ${model} refused to analyze image${i < models.length - 1 ? ', trying fallback...' : ''}`);
} catch (error) {
// API error (not a refusal) - still try fallback
this.logger.warn(`Model ${model} failed: ${error instanceof Error ? error.message : error}${i < models.length - 1 ? ', trying fallback...' : ''}`);
}
}
// All models refused or failed
throw new Error('AI_REFUSAL: All models refused to analyze image');
}
private async tryAnalyzeWithModel(model: string, imageBase64: string, mimeType: string): Promise<ImageAnalysisResult | null> {
const genderPrompt = `First, identify the gender of the person in this image. Respond with EXACTLY one of these on the first line: GENDER:male, GENDER:female, GENDER:other, or GENDER:unknown
Then provide your analysis:
${this.config.openrouter.imageAnalysisPrompt}`;
const response = await this.client.chat.completions.create({
model: this.config.openrouter.imageAnalysisModel,
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: genderPrompt,
const response = await this.client.chat.completions.create({
model,
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: genderPrompt,
},
{
type: 'image_url',
image_url: {
url: `data:${mimeType};base64,${imageBase64}`,
},
{
type: 'image_url',
image_url: {
url: `data:${mimeType};base64,${imageBase64}`,
},
},
],
},
],
max_tokens: 600,
});
},
],
},
],
max_tokens: 600,
});
const fullResponse = response.choices[0]?.message?.content || '';
const fullResponse = response.choices[0]?.message?.content || '';
// Parse gender from response
let detectedGender: DetectedGender = 'unknown';
const genderMatch = fullResponse.match(/GENDER:(male|female|other|unknown)/i);
if (genderMatch) {
detectedGender = genderMatch[1].toLowerCase() as DetectedGender;
}
// Remove gender line from description
const description = fullResponse.replace(/GENDER:(male|female|other|unknown)\n?/i, '').trim();
this.logger.debug(`Image analysis complete - Gender: ${detectedGender}`);
this.logger.debug(`Description: ${description}`);
return { description, detectedGender };
} catch (error) {
this.logger.error('Image analysis failed:', error);
throw error;
// Parse gender from response
let detectedGender: DetectedGender = 'unknown';
const genderMatch = fullResponse.match(/GENDER:(male|female|other|unknown)/i);
if (genderMatch) {
detectedGender = genderMatch[1].toLowerCase() as DetectedGender;
}
// Remove gender line from description
const description = fullResponse.replace(/GENDER:(male|female|other|unknown)\n?/i, '').trim();
// Detect AI refusal - return null to signal fallback should be tried
if (this.isRefusal(description)) {
return null;
}
this.logger.debug(`Image analysis complete (${model}) - Gender: ${detectedGender}`);
this.logger.debug(`Description: ${description}`);
return { description, detectedGender };
}
private isRefusal(text: string): boolean {
const lower = text.toLowerCase();
const refusalPatterns = [
'i cannot fulfill',
'i can\'t fulfill',
'i cannot provide',
'i can\'t provide',
'i\'m not able to',
'i am not able to',
'my safety guidelines',
'i\'m programmed to',
'i am programmed to',
'i cannot generate',
'i can\'t generate',
'against my guidelines',
'i must decline',
'i cannot assist with',
'i can\'t assist with',
'i\'m unable to',
'i am unable to',
'violates my',
'not appropriate for me',
'i cannot engage',
'i can\'t engage',
'sexually explicit',
'objectification',
'prohibit me from',
];
return refusalPatterns.some(pattern => lower.includes(pattern));
}
async generateMessage(
imageAnalysis: string,
postTitle: string,
authorName: string,
successfulExamples: string[] = []
successfulExamples: string[] = [],
strategyPrompt?: string
): Promise<string> {
try {
this.logger.debug('Generating message...');
@@ -132,6 +199,11 @@ ${this.config.openrouter.imageAnalysisPrompt}`;
.replace('{title}', postTitle)
.replace('{author}', authorName);
// Append strategy-specific style guidance if provided
if (strategyPrompt) {
prompt += `\n\n${strategyPrompt}`;
}
// Add successful examples if available
if (successfulExamples.length > 0) {
const examplesText = successfulExamples
@@ -159,15 +231,77 @@ ${this.config.openrouter.imageAnalysisPrompt}`;
}
}
async generateReply(
conversationHistory: { sender: string; text: string; isSelf: boolean }[],
username: string
): Promise<string> {
try {
this.logger.debug(`Generating suggested reply for conversation with ${username}...`);
const historyText = conversationHistory
.map(msg => `${msg.isSelf ? 'You' : msg.sender}: ${msg.text}`)
.join('\n');
const prompt = `You are helping continue a Reddit chat conversation. Here is the conversation so far:
${historyText}
Write the next message to send to ${username}. Keep the same tone and style as the previous messages from "You". Be casual, direct, and engaging. Keep it to 1-3 sentences. Reference something they said in their reply to keep the conversation flowing naturally.
DO NOT:
- Sound like an AI or use formal language
- Use words like "captivating", "alluring", "exquisite", "radiant"
- Write long elaborate sentences
- Be generic or boring
Just write the message, nothing else.`;
const response = await this.client.chat.completions.create({
model: this.config.openrouter.chatModel,
messages: [{ role: 'user', content: prompt }],
max_tokens: 300,
temperature: 0.8,
});
const message = response.choices[0]?.message?.content || '';
this.logger.debug(`Suggested reply generated: ${message}`);
return message.trim();
} catch (error) {
this.logger.error('Reply generation failed:', error);
throw error;
}
}
/**
* Check if message content contains upsell/spam keywords (OnlyFans, selling, etc.)
*/
detectUpsell(messages: { text: string; isSelf: boolean }[], upsellKeywords: string[]): { isUpsell: boolean; matchedKeyword?: string } {
// Only check messages from the other user
const theirMessages = messages.filter(m => !m.isSelf);
if (theirMessages.length === 0) return { isUpsell: false };
// Combine all their message text for keyword scanning
const combinedText = theirMessages.map(m => m.text).join(' ').toLowerCase();
for (const keyword of upsellKeywords) {
if (combinedText.includes(keyword.toLowerCase())) {
return { isUpsell: true, matchedKeyword: keyword };
}
}
return { isUpsell: false };
}
async analyzeAndGenerateMessage(
imageBase64: string,
mimeType: string,
postTitle: string,
authorName: string,
successfulExamples: string[] = []
successfulExamples: string[] = [],
strategyPrompt?: string
): Promise<{ analysis: ImageAnalysisResult; message: string }> {
const analysis = await this.analyzeImage(imageBase64, mimeType);
const message = await this.generateMessage(analysis.description, postTitle, authorName, successfulExamples);
const message = await this.generateMessage(analysis.description, postTitle, authorName, successfulExamples, strategyPrompt);
return { analysis, message };
}
}