feat: 인물 추적에서 음악방송 무대·직캠·커버 제외

아이돌을 인물로 추적해봤더니 안 먹힌다는 걸 실측으로 확인했다.

- 박지원 → 상위 3건이 전부 정치인 박지원(위대한 이슈 12.9만, KBC/JTV 뉴스).
  동명이인 때문에 이름만으로는 못 쓴다.
- 송하영·오해원·프로미스나인 → 잡히는 게 전부 음악방송 직캠·무대·팬캠.
  예능 출연분이 직캠에 묻힌다.

배우·코미디언(윤경호·소지섭·딘딘)은 예능/드라마 클립이 상위에 오는데 아이돌은
검색 자체가 직캠 판이라 구조가 다르다. 그래서 두 가지를 했다.

1) 제목에 직캠·페이스캠·풀캠·교차편집·음중·PERFORMANCE VIDEO·커버 등이 들어가면
   버린다. 말로 터지는 순간이 소재라 무대 영상은 소용없다. 적용 후 이채영
   결과가 예능만 남았다(공포체험 4.9만, 고부갈등, 아이돌 인간극장).
2) 아이돌 소재는 인물이 아니라 채널로 잡는 게 맞다. fromis_9 공식채널을 SOURCE
   시드로 추가 — 자체 예능 롱폼이 나온다([FM_1.24] 31분, Answer ME! 고민상담소 15분).

테스트 2건 추가 — 총 94건 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-08-02 23:10:33 +09:00
parent 7a954907d7
commit c0dbd3b23d
2 changed files with 49 additions and 1 deletions

View File

@ -13,8 +13,29 @@ import java.util.Set;
*/ */
public final class PersonPicks { public final class PersonPicks {
/**
* 제목에 이게 들어가면 버린다. 아이돌 이름으로 검색하면 음악방송 직캠·무대·교차편집이
* 결과를 뒤덮어 정작 예능 출연분이 묻힌다. 말로 터지는 순간이 소재이므로 무대 영상은 소용없다.
*/
private static final List<String> STAGE_MARKERS = List.of(
"직캠", "페이스캠", "풀캠", "fancam", "focus cam",
"교차편집", "stage mix", "stagemix", "무대교차",
"음중", "쇼챔", "엠카", "뮤직뱅크", "인기가요", "music bank", "show champion",
"performance video", "mv", "m/v", "lyric video", "cover", "커버"
);
private PersonPicks() {} private PersonPicks() {}
/** 음악방송 무대·직캠·커버 영상인가(제목 기준). */
public static boolean isStageVideo(String title) {
if (title == null || title.isBlank()) return false;
String t = title.toLowerCase(java.util.Locale.ROOT);
for (String marker : STAGE_MARKERS) {
if (t.contains(marker)) return true;
}
return false;
}
/** /**
* 검색 결과 1건. * 검색 결과 1건.
* *
@ -32,7 +53,7 @@ public final class PersonPicks {
* @param minViewsPerHour 시간당 조회수 하한. 인물명 검색에는 팬채널·커버곡·개인 브이로그가 * @param minViewsPerHour 시간당 조회수 하한. 인물명 검색에는 팬채널·커버곡·개인 브이로그가
* 대량으로 딸려 오는데(실측 조회수 30회 미만), 조회수로 자르면 방금 올라온 * 대량으로 딸려 오는데(실측 조회수 30회 미만), 조회수로 자르면 방금 올라온
* 영상까지 걸리므로 시간당 조회수로 거른다. 0 이하면 미적용 * 영상까지 걸리므로 시간당 조회수로 거른다. 0 이하면 미적용
* @return 롱폼이면서 제외 대상이 아닌 것들 (입력 순서 유지) * @return 롱폼이면서 무대 영상이 아니고 제외 대상도 아닌 것들 (입력 순서 유지)
*/ */
public static List<Found> keep(List<Found> found, Set<String> excludeChannels, public static List<Found> keep(List<Found> found, Set<String> excludeChannels,
LocalDateTime publishedAfter, double minViewsPerHour) { LocalDateTime publishedAfter, double minViewsPerHour) {
@ -47,6 +68,7 @@ public final class PersonPicks {
&& excludeChannels.contains(f.ytChannelId())) continue; && excludeChannels.contains(f.ytChannelId())) continue;
if (publishedAfter != null && f.publishedAt() != null if (publishedAfter != null && f.publishedAt() != null
&& f.publishedAt().isBefore(publishedAfter)) continue; && f.publishedAt().isBefore(publishedAfter)) continue;
if (isStageVideo(f.title())) continue;
if (minViewsPerHour > 0 && !meetsVelocity(f, minViewsPerHour)) continue; if (minViewsPerHour > 0 && !meetsVelocity(f, minViewsPerHour)) continue;
out.add(f); out.add(f);
} }

View File

@ -90,6 +90,32 @@ class PersonPicksTest {
assertThat(PersonPicks.keep(List.of(unknown), Set.of(), null, 20)).hasSize(1); assertThat(PersonPicks.keep(List.of(unknown), Set.of(), null, 20)).hasSize(1);
} }
@Test
void 음악방송_무대_직캠_커버는_버린다() {
// 아이돌 이름으로 검색하면 직캠·무대가 결과를 뒤덮는다. 말로 터지는 순간이 소재라 무대는 소용없다
assertThat(PersonPicks.isStageVideo("[#음중직캠] fromis_9 LEE CHAEYOUNG Vitamin ME")).isTrue();
assertThat(PersonPicks.isStageVideo("[페이스캠4K] 프로미스나인 이채영")).isTrue();
assertThat(PersonPicks.isStageVideo("fromis_9 'Vitamin ME' PERFORMANCE VIDEO")).isTrue();
assertThat(PersonPicks.isStageVideo("[cover] 딘딘 - 태양은 가득히")).isTrue();
assertThat(PersonPicks.isStageVideo("교차편집 stage mix")).isTrue();
// 예능·드라마 클립은 남아야 한다
assertThat(PersonPicks.isStageVideo("이채영 임슬옹 공포체험 촬영 중단!?")).isFalse();
assertThat(PersonPicks.isStageVideo("남우조연상 - 윤경호 [제5회 청룡시리즈어워즈]")).isFalse();
assertThat(PersonPicks.isStageVideo(null)).isFalse();
}
@Test
void keep은_무대영상을_걸러낸다() {
PersonPicks.Found stage = new PersonPicks.Found(
"s", "[#음중직캠] 프로미스나인 이채영", "C1", "MBCkpop", 200, NOW, 50_000L);
PersonPicks.Found variety = new PersonPicks.Found(
"v", "이채영 임슬옹 공포체험 촬영 중단!?", "C2", "OngStyle", 200, NOW, 50_000L);
assertThat(PersonPicks.keep(List.of(stage, variety), Set.of(), null, 0))
.extracting(PersonPicks.Found::videoId).containsExactly("v");
}
@Test @Test
void null_입력과_빈_videoId_방어() { void null_입력과_빈_videoId_방어() {
List<PersonPicks.Found> found = new java.util.ArrayList<>(); List<PersonPicks.Found> found = new java.util.ArrayList<>();