이 날은 유독 왔다갔다가 많았다.
기승전결 4박자 구조에서 [기]와 [전]이 둘 다 "~다고 함" 계열로 끝나거나, [승]과 [전]이 둘 다 "~라고" 계열로 끝나는 경우가 있었다. 같은 계열이 바로 옆에 붙으면 문장이 단조로워진다. parseOutput에 계열 분류기를 추가했다.
// A 계열: ~[다라]고 함 (있다고 함 / 했다고 함 / 이라고 함 등)
// B 계열: ~라고 (상황이라고 / 이라고 — 단 '하는데'·'함'이 뒤에 없는 직접 종결)
const classify = (s: string): 'A' | 'B' | 'other' => {
if (/[다라]고 함$/.test(s)) return 'A';
if (/라고$/.test(s)) return 'B';
return 'other';
};
for (let i = 0; i < sentences.length - 1; i++) {
const curr = classify(sentences[i]);
const next = classify(sentences[i + 1]);
if (curr !== 'other' && curr === next) {
throw new Error('SCRIPT_CONSECUTIVE_ENDING');
}
}
isRetryable에도 추가해서 위반하면 자동 재시도한다.
"~다는, 상황이라고" 같은 문장이 나왔다. "~다는" 뒤에 쉼표를 찍으면 한국어로 어색하다. 정규식으로 걸렀다.
if (parsed.script && /(다는|이라는|있다는|없다는|한다는|됐다는),/.test(parsed.script)) {
throw new Error('SCRIPT_AWKWARD_COMMA');
}
카테고리에서 politics를 없앤 지 한 달이 넘었는데, 다른 카테고리(top 등) 뉴스에도 정당명이나 정치인 이름이 섞여 들어오는 경우가 있었다. RSS 후보를 20개까지 넉넉히 받아서 정치 키워드로 거른 다음 필요한 개수만 슬라이스하도록 바꿨다.
const BLOCK_KEYWORDS = [
'민주당', '국민의힘', '정의당', '개혁신당', '조국혁신당',
'이재명', '한동훈', '윤석열',
'여당', '야당',
'총선', '대선', '지방선거',
'당대표', '원내대표', '국회의원',
'탄핵', '특검',
];
export async function fetchNewsTopics(category: NewsCategory, count: number): Promise<NewsItem[]> {
const xml = await fetchUrl(CATEGORY_URLS[category]);
const candidates = parseRssItems(xml, CANDIDATES_PER_FETCH);
return candidates.filter((item) => !isPolitical(item.title)).slice(0, count);
}
여기부터가 오늘의 하이라이트다.
13:47에 ko-KR-SunHiNeural(여성)에서 ko-KR-InJoonNeural(남성)로 바꿔서 배포했다.
await tts.setMetadata('ko-KR-InJoonNeural', OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3);
근데 msedge-tts 라이브러리가 이 보이스를 제대로 지원하는지 실제로 확인을 안 한 채로 올렸다는 걸 뒤늦게 깨달았다. Edge TTS 호환성이 검증 안 된 상태로 프로덕션에 나가 있는 게 찜찜해서, 17분 만에 그냥 SunHiNeural로 되돌렸다.
-'ko-KR-InJoonNeural',
+'ko-KR-SunHiNeural',
그 사이에 dlq-notifier와 CI Slack 알림 작업을 잠깐 하고, 확인이 끝나자 다시 InJoonNeural로 돌려놨다. 결국 최종 상태는 처음 교체하려던 것과 같다. 급하게 올렸다가 찜찜해서 되돌리고, 확인 후 재적용하는 흐름이었다.
같은 타이밍에 dlq-notifier가 비정상 JSON 메시지를 받았을 때 jobId/channelId를 못 뽑아내던 것도 고쳤다. JSON.parse가 실패하면 정규식으로라도 건져내게 했다.
} catch {
jobId = record.body.match(/jobId[:"' ]*([^,}\s'"]+)/)?.[1] ?? '알 수 없음';
channelId = record.body.match(/channelId[:"' ]*([^,}\s'"]+)/)?.[1] ?? '알 수 없음';
}
그리고 배포 완료·실패를 Slack으로 알려주는 것도 이날 처음 붙였다. deploy-api, deploy-web, deploy-workers 세 워크플로우 전부에.
- name: Slack 배포 결과 알림
if: always()
run: |
if [ "$STATUS" = "success" ]; then ICON="✅"; else ICON="❌"; fi
curl -s -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
-H "Content-Type: application/json" \
-d "{\"text\":\"$ICON *Deploy API* \`$STATUS\`\n• Ref: \`$REF\` @ \`${SHA::7}\`\"}"
이게 다음 날부터 Slack 알림을 계속 다듬게 되는 시작점이 됐다.