Add editable weather captions and manage Threads scheduling with PM2

This commit is contained in:
2026-11-02 04:07:40 +09:00
parent 8c2113a323
commit 6ce32d33fe
17 changed files with 305 additions and 50 deletions
+7
View File
@@ -7,6 +7,13 @@ export const THREADS = {
mediaPollMs: 5_000,
mediaPollAttempts: 24,
tokenRefreshMs: 7 * 86_400_000,
captionMaxLength: 500,
captionConditions: ['clear', 'partlyCloudy', 'cloudy', 'fog', 'rain', 'snow', 'unknown'],
scheduleHourKst: 3,
retryAttempts: 5,
retryMs: 60_000,
jobTimeoutMs: 8 * 60 * 60_000,
jobStopGraceMs: 10_000,
viewport: { width: 1080, height: 1728 },
angles: [
{ name: 'sunrise', offset: 0, pitch: 0, alt: '일출을 바라보는 캐릭터의 뒷모습과 관악산 풍경' },
+3 -1
View File
@@ -128,8 +128,10 @@ export async function start(options: StartOptions = {}) {
world.update(state);
};
const weatherStatus = document.getElementById('weather-status')!;
let captureWeather: WeatherStatus = { status: 'loading', weather: null };
const applyWeather = ({ status, weather, source }: WeatherStatus) => {
if (import.meta.env.DEV && new URLSearchParams(location.search).has('profile')) return;
captureWeather = { status, weather, source };
world.setWeather(weather);
weatherStatus.textContent = status === 'ready' && weather
? `${source} · 운량 ${Math.round(weather.cloudCover)}% · ${weather.visibility === null
@@ -243,7 +245,7 @@ export async function start(options: StartOptions = {}) {
window.addEventListener('pageshow', event => { if (event.persisted && sharedMode) join(); });
if (query.get('capture') === 'threads' && !sharedMode) {
const { installThreadsCapture } = await import('./ui/threadsCapture.ts');
installThreadsCapture(effectiveObserver, world);
installThreadsCapture(effectiveObserver, world, () => captureWeather);
}
options.ready?.();
} catch (error) {
+7 -3
View File
@@ -1,18 +1,21 @@
import type { ObserverConfig } from '../config/observer.ts';
import { calculateSun, sunriseAzimuth } from '../astronomy/sunCalculator.ts';
import { THREADS } from '../constants/threads.ts';
import type { WeatherSample, WeatherStatus } from '../weather/weather.ts';
export interface ThreadsCapture {
sunrise(): string;
capture(): { images: { name: string; image: string }[]; capturedAt: string };
weatherStatus(): WeatherStatus['status'];
capture(): { images: { name: string; image: string }[]; capturedAt: string; weather: WeatherSample | null };
}
declare global { interface Window { sunriseThreads?: ThreadsCapture } }
export function installThreadsCapture(observer: ObserverConfig,
world: { update: (state: ReturnType<typeof calculateSun>) => void; lookAtSunrise: (azimuth: number) => void;
captureAngle: (azimuth: number, pitch: number) => HTMLCanvasElement }) {
captureAngle: (azimuth: number, pitch: number) => HTMLCanvasElement }, getWeather: () => WeatherStatus) {
window.sunriseThreads = {
weatherStatus: () => getWeather().status,
sunrise() {
const sunrise = calculateSun(new Date(), observer).sunrise;
if (!sunrise || !Number.isFinite(+sunrise)) throw new Error('No sunrise');
@@ -25,7 +28,8 @@ export function installThreadsCapture(observer: ObserverConfig,
world.lookAtSunrise(azimuth);
const images = THREADS.angles.map(angle => ({ name: angle.name,
image: world.captureAngle((azimuth + angle.offset) % 360, angle.pitch).toDataURL('image/jpeg', 0.95) }));
return { images, capturedAt: now.toISOString() };
const weather = getWeather();
return { images, capturedAt: now.toISOString(), weather: weather.status === 'ready' ? weather.weather : null };
},
};
}