Files
sunrise/server/threads/captions.ts
T

62 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFileSync } from 'node:fs';
import type { WeatherSample } from '../../src/weather/weather.ts';
import { kstClock, kstFields } from '../../src/ui/time.ts';
import { THREADS } from '../../src/constants/threads.ts';
type Condition = typeof THREADS.captionConditions[number];
export interface Captions {
template: string[];
testPrefix: string;
thresholds: { fogVisibilityMeters: number; cloudyPercent: number; partlyCloudyPercent: number };
messages: Record<Condition, string[]>;
}
export function loadCaptions(path: string | URL = new URL('../../.threads-captions.json', import.meta.url)): Captions {
let text: string;
try { text = readFileSync(path, 'utf8'); }
catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
text = readFileSync(new URL('../../.threads-captions.sample.json', import.meta.url), 'utf8');
}
let config: Captions;
try { config = JSON.parse(text); } catch { throw new Error('Invalid JSON in .threads-captions.json'); }
const lines = (value: unknown): value is string[] => Array.isArray(value) && value.length > 0 && value.every(line => typeof line === 'string');
if (!config || !lines(config.template) || typeof config.testPrefix !== 'string'
|| !THREADS.captionConditions.every(key => lines(config.messages?.[key]))) throw new Error('Invalid caption template or weather messages');
const t = config.thresholds;
if (!t || !Number.isFinite(t.fogVisibilityMeters) || t.fogVisibilityMeters < 0
|| !Number.isFinite(t.partlyCloudyPercent) || !Number.isFinite(t.cloudyPercent)
|| t.partlyCloudyPercent < 0 || t.cloudyPercent > 100 || t.partlyCloudyPercent >= t.cloudyPercent) {
throw new Error('Invalid caption weather thresholds');
}
return config;
}
export function weatherKey(weather: WeatherSample | null, config: Captions): Condition {
if (!weather) return 'unknown';
if ((weather.snowfall ?? 0) > 0) return 'snow';
if ((weather.rain ?? 0) > 0) return 'rain';
if (weather.visibility !== null && weather.visibility < config.thresholds.fogVisibilityMeters) return 'fog';
if (weather.rain === null || weather.snowfall === null || !Number.isFinite(weather.cloudCover)) return 'unknown';
if (weather.cloudCover >= config.thresholds.cloudyPercent) return 'cloudy';
if (weather.cloudCover >= config.thresholds.partlyCloudyPercent) return 'partlyCloudy';
return 'clear';
}
export function renderCaption(config: Captions, capture: {
weather: WeatherSample | null; capturedAt: string; sunrise: string; url: string; test: boolean;
}) {
const condition = weatherKey(capture.weather, config);
const date = new Date(capture.capturedAt);
const values: Record<string, string> = {
message: config.messages[condition].join('\n'), date: kstFields(date).date,
time: kstClock(date).slice(0, 5), sunrise: kstClock(new Date(capture.sunrise)).slice(0, 5), url: capture.url,
};
const text = (capture.test ? config.testPrefix : '') + config.template.join('\n').replace(/\{(\w+)\}/g, (_, key: string) => {
if (!(key in values)) throw new Error(`Unknown caption placeholder: ${key}`);
return values[key];
});
if (!text.trim() || [...text].length > THREADS.captionMaxLength) throw new Error('Caption must contain 1–500 characters');
return { text, condition };
}