mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -07:00
fix(stats): use yomitan tokens for subtitle counts
This commit is contained in:
@@ -30,7 +30,6 @@ test('buildOverviewSummary aggregates tracked totals and recent windows', () =>
|
||||
totalWatchedMs: 3_600_000,
|
||||
activeWatchedMs: 3_000_000,
|
||||
linesSeen: 20,
|
||||
wordsSeen: 100,
|
||||
tokensSeen: 80,
|
||||
cardsMined: 2,
|
||||
lookupCount: 10,
|
||||
@@ -45,33 +44,32 @@ test('buildOverviewSummary aggregates tracked totals and recent windows', () =>
|
||||
totalSessions: 1,
|
||||
totalActiveMin: 50,
|
||||
totalLinesSeen: 20,
|
||||
totalWordsSeen: 100,
|
||||
totalTokensSeen: 80,
|
||||
totalCards: 2,
|
||||
cardsPerHour: 2.4,
|
||||
wordsPerMin: 2,
|
||||
tokensPerMin: 2,
|
||||
lookupHitRate: 0.8,
|
||||
},
|
||||
];
|
||||
const overview: OverviewData = {
|
||||
sessions,
|
||||
rollups,
|
||||
hints: {
|
||||
totalSessions: 15,
|
||||
activeSessions: 0,
|
||||
episodesToday: 2,
|
||||
activeAnimeCount: 3,
|
||||
totalEpisodesWatched: 5,
|
||||
totalAnimeCompleted: 1,
|
||||
totalActiveMin: 50,
|
||||
activeDays: 2,
|
||||
totalCards: 9,
|
||||
totalLookupCount: 100,
|
||||
totalLookupHits: 80,
|
||||
newWordsToday: 5,
|
||||
newWordsThisWeek: 20,
|
||||
},
|
||||
};
|
||||
const overview: OverviewData = {
|
||||
sessions,
|
||||
rollups,
|
||||
hints: {
|
||||
totalSessions: 15,
|
||||
activeSessions: 0,
|
||||
episodesToday: 2,
|
||||
activeAnimeCount: 3,
|
||||
totalEpisodesWatched: 5,
|
||||
totalAnimeCompleted: 1,
|
||||
totalActiveMin: 50,
|
||||
activeDays: 2,
|
||||
totalCards: 9,
|
||||
totalLookupCount: 100,
|
||||
totalLookupHits: 80,
|
||||
newWordsToday: 5,
|
||||
newWordsThisWeek: 20,
|
||||
},
|
||||
};
|
||||
|
||||
const summary = buildOverviewSummary(overview, now);
|
||||
assert.equal(summary.todayCards, 2);
|
||||
@@ -88,8 +86,8 @@ test('buildOverviewSummary aggregates tracked totals and recent windows', () =>
|
||||
test('buildOverviewSummary prefers lifetime totals from hints when provided', () => {
|
||||
const now = Date.UTC(2026, 2, 13, 12);
|
||||
const today = Math.floor(now / 86_400_000);
|
||||
const overview: OverviewData = {
|
||||
sessions: [
|
||||
const overview: OverviewData = {
|
||||
sessions: [
|
||||
{
|
||||
sessionId: 2,
|
||||
canonicalTitle: 'B',
|
||||
@@ -101,7 +99,6 @@ test('buildOverviewSummary prefers lifetime totals from hints when provided', ()
|
||||
totalWatchedMs: 60_000,
|
||||
activeWatchedMs: 60_000,
|
||||
linesSeen: 10,
|
||||
wordsSeen: 10,
|
||||
tokensSeen: 10,
|
||||
cardsMined: 10,
|
||||
lookupCount: 1,
|
||||
@@ -116,11 +113,10 @@ test('buildOverviewSummary prefers lifetime totals from hints when provided', ()
|
||||
totalSessions: 1,
|
||||
totalActiveMin: 1,
|
||||
totalLinesSeen: 10,
|
||||
totalWordsSeen: 10,
|
||||
totalTokensSeen: 10,
|
||||
totalCards: 10,
|
||||
cardsPerHour: 600,
|
||||
wordsPerMin: 10,
|
||||
tokensPerMin: 10,
|
||||
lookupHitRate: 1,
|
||||
},
|
||||
],
|
||||
@@ -182,11 +178,10 @@ test('buildTrendDashboard derives dense chart series', () => {
|
||||
totalSessions: 2,
|
||||
totalActiveMin: 60,
|
||||
totalLinesSeen: 30,
|
||||
totalWordsSeen: 120,
|
||||
totalTokensSeen: 100,
|
||||
totalCards: 3,
|
||||
cardsPerHour: 3,
|
||||
wordsPerMin: 2,
|
||||
tokensPerMin: 2,
|
||||
lookupHitRate: 0.5,
|
||||
},
|
||||
{
|
||||
@@ -195,18 +190,17 @@ test('buildTrendDashboard derives dense chart series', () => {
|
||||
totalSessions: 1,
|
||||
totalActiveMin: 30,
|
||||
totalLinesSeen: 10,
|
||||
totalWordsSeen: 40,
|
||||
totalTokensSeen: 30,
|
||||
totalCards: 1,
|
||||
cardsPerHour: 2,
|
||||
wordsPerMin: 1.33,
|
||||
tokensPerMin: 1.33,
|
||||
lookupHitRate: 0.75,
|
||||
},
|
||||
];
|
||||
|
||||
const dashboard = buildTrendDashboard(rollups);
|
||||
assert.equal(dashboard.watchTime.length, 2);
|
||||
assert.equal(dashboard.words[1]?.value, 40);
|
||||
assert.equal(dashboard.words[1]?.value, 30);
|
||||
assert.equal(dashboard.sessions[0]?.value, 2);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface OverviewSummary {
|
||||
activeDays: number;
|
||||
totalSessions: number;
|
||||
lookupRate: number | null;
|
||||
todayWords: number;
|
||||
todayTokens: number;
|
||||
newWordsToday: number;
|
||||
newWordsThisWeek: number;
|
||||
recentWatchTime: ChartPoint[];
|
||||
@@ -100,7 +100,7 @@ function buildAggregatedDailyRows(rollups: DailyRollup[]) {
|
||||
|
||||
existing.activeMin += rollup.totalActiveMin;
|
||||
existing.cards += rollup.totalCards;
|
||||
existing.words += rollup.totalWordsSeen;
|
||||
existing.words += rollup.totalTokensSeen;
|
||||
existing.sessions += rollup.totalSessions;
|
||||
if (rollup.lookupHitRate != null) {
|
||||
const weight = Math.max(rollup.totalSessions, 1);
|
||||
@@ -185,9 +185,9 @@ export function buildOverviewSummary(
|
||||
overview.hints.totalLookupCount > 0
|
||||
? Math.round((overview.hints.totalLookupHits / overview.hints.totalLookupCount) * 100)
|
||||
: null,
|
||||
todayWords: Math.max(
|
||||
todayTokens: Math.max(
|
||||
todayRow?.words ?? 0,
|
||||
sumBy(todaySessions, (session) => session.wordsSeen),
|
||||
sumBy(todaySessions, (session) => session.tokensSeen),
|
||||
),
|
||||
newWordsToday: overview.hints.newWordsToday ?? 0,
|
||||
newWordsThisWeek: overview.hints.newWordsThisWeek ?? 0,
|
||||
|
||||
@@ -18,7 +18,6 @@ test('MediaSessionList renders expandable session rows with delete affordance',
|
||||
totalWatchedMs: 1_000,
|
||||
activeWatchedMs: 900,
|
||||
linesSeen: 12,
|
||||
wordsSeen: 24,
|
||||
tokensSeen: 24,
|
||||
cardsMined: 2,
|
||||
lookupCount: 3,
|
||||
@@ -34,6 +33,6 @@ test('MediaSessionList renders expandable session rows with delete affordance',
|
||||
assert.match(markup, /Session History/);
|
||||
assert.match(markup, /aria-expanded="true"/);
|
||||
assert.match(markup, /Delete session Episode 7/);
|
||||
assert.match(markup, /Total words/);
|
||||
assert.match(markup, /1 Yomitan lookup/);
|
||||
assert.match(markup, /tokens/);
|
||||
assert.match(markup, /No token data for this session/);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { SessionDetail } from '../components/sessions/SessionDetail';
|
||||
import { buildSessionChartEvents } from './session-events';
|
||||
import { EventType } from '../types/stats';
|
||||
|
||||
test('SessionDetail omits the misleading new words metric', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
@@ -17,7 +19,6 @@ test('SessionDetail omits the misleading new words metric', () => {
|
||||
totalWatchedMs: 0,
|
||||
activeWatchedMs: 0,
|
||||
linesSeen: 12,
|
||||
wordsSeen: 24,
|
||||
tokensSeen: 24,
|
||||
cardsMined: 0,
|
||||
lookupCount: 0,
|
||||
@@ -27,6 +28,33 @@ test('SessionDetail omits the misleading new words metric', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
assert.match(markup, /Total words/);
|
||||
assert.match(markup, /No token data/);
|
||||
assert.doesNotMatch(markup, /New words/);
|
||||
});
|
||||
|
||||
test('buildSessionChartEvents keeps only chart-relevant events and pairs pause ranges', () => {
|
||||
const chartEvents = buildSessionChartEvents([
|
||||
{ eventType: EventType.SUBTITLE_LINE, tsMs: 1_000, payload: '{"line":"ignored"}' },
|
||||
{ eventType: EventType.PAUSE_START, tsMs: 2_000, payload: null },
|
||||
{ eventType: EventType.SEEK_FORWARD, tsMs: 3_000, payload: null },
|
||||
{ eventType: EventType.PAUSE_END, tsMs: 4_000, payload: null },
|
||||
{ eventType: EventType.CARD_MINED, tsMs: 5_000, payload: '{"cardsMined":1}' },
|
||||
{ eventType: EventType.YOMITAN_LOOKUP, tsMs: 6_000, payload: null },
|
||||
{ eventType: EventType.SEEK_BACKWARD, tsMs: 7_000, payload: null },
|
||||
{ eventType: EventType.LOOKUP, tsMs: 8_000, payload: '{"hit":true}' },
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
chartEvents.seekEvents.map((event) => event.eventType),
|
||||
[EventType.SEEK_FORWARD, EventType.SEEK_BACKWARD],
|
||||
);
|
||||
assert.deepEqual(
|
||||
chartEvents.cardEvents.map((event) => event.tsMs),
|
||||
[5_000],
|
||||
);
|
||||
assert.deepEqual(
|
||||
chartEvents.yomitanLookupEvents.map((event) => event.tsMs),
|
||||
[6_000],
|
||||
);
|
||||
assert.deepEqual(chartEvents.pauseRegions, [{ startMs: 2_000, endMs: 4_000 }]);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
type SessionWordCountLike = {
|
||||
wordsSeen: number;
|
||||
tokensSeen: number;
|
||||
};
|
||||
|
||||
export function getSessionDisplayWordCount(value: SessionWordCountLike): number {
|
||||
return value.tokensSeen > 0 ? value.tokensSeen : value.wordsSeen;
|
||||
return value.tokensSeen;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ test('EpisodeList renders explicit episode detail button alongside quick peek ro
|
||||
totalSessions: 1,
|
||||
totalActiveMs: 1,
|
||||
totalCards: 1,
|
||||
totalWordsSeen: 350,
|
||||
totalTokensSeen: 350,
|
||||
totalYomitanLookupCount: 7,
|
||||
lastWatchedMs: 0,
|
||||
},
|
||||
|
||||
@@ -8,10 +8,10 @@ import { SessionRow } from '../components/sessions/SessionRow';
|
||||
import { EventType, type SessionEvent } from '../types/stats';
|
||||
import { buildLookupRateDisplay, getYomitanLookupEvents } from './yomitan-lookup';
|
||||
|
||||
test('buildLookupRateDisplay formats lookups per 100 words in short and long forms', () => {
|
||||
test('buildLookupRateDisplay formats lookups per 100 tokens in short and long forms', () => {
|
||||
assert.deepEqual(buildLookupRateDisplay(23, 1000), {
|
||||
shortValue: '2.3 / 100 words',
|
||||
longValue: '2.3 lookups per 100 words',
|
||||
shortValue: '2.3 / 100 tokens',
|
||||
longValue: '2.3 lookups per 100 tokens',
|
||||
});
|
||||
assert.equal(buildLookupRateDisplay(0, 0), null);
|
||||
});
|
||||
@@ -38,7 +38,7 @@ test('MediaHeader renders Yomitan lookup count and lookup rate copy', () => {
|
||||
totalSessions: 4,
|
||||
totalActiveMs: 90_000,
|
||||
totalCards: 12,
|
||||
totalWordsSeen: 1000,
|
||||
totalTokensSeen: 1000,
|
||||
totalLinesSeen: 120,
|
||||
totalLookupCount: 30,
|
||||
totalLookupHits: 21,
|
||||
@@ -48,11 +48,11 @@ test('MediaHeader renders Yomitan lookup count and lookup rate copy', () => {
|
||||
);
|
||||
|
||||
assert.match(markup, /23/);
|
||||
assert.match(markup, /2\.3 \/ 100 words/);
|
||||
assert.match(markup, /2\.3 lookups per 100 words/);
|
||||
assert.match(markup, /2\.3 \/ 100 tokens/);
|
||||
assert.match(markup, /2\.3 lookups per 100 tokens/);
|
||||
});
|
||||
|
||||
test('MediaHeader distinguishes word occurrences from known unique words', () => {
|
||||
test('MediaHeader distinguishes token occurrences from known unique words', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<MediaHeader
|
||||
detail={{
|
||||
@@ -61,7 +61,7 @@ test('MediaHeader distinguishes word occurrences from known unique words', () =>
|
||||
totalSessions: 4,
|
||||
totalActiveMs: 90_000,
|
||||
totalCards: 12,
|
||||
totalWordsSeen: 30,
|
||||
totalTokensSeen: 30,
|
||||
totalLinesSeen: 120,
|
||||
totalLookupCount: 30,
|
||||
totalLookupHits: 21,
|
||||
@@ -74,7 +74,7 @@ test('MediaHeader distinguishes word occurrences from known unique words', () =>
|
||||
/>,
|
||||
);
|
||||
|
||||
assert.match(markup, /word occurrences/);
|
||||
assert.match(markup, /token occurrences/);
|
||||
assert.match(markup, /known unique words \(50%\)/);
|
||||
assert.match(markup, /17 \/ 34/);
|
||||
});
|
||||
@@ -93,7 +93,7 @@ test('EpisodeList renders per-episode Yomitan lookup rate', () => {
|
||||
totalSessions: 1,
|
||||
totalActiveMs: 1,
|
||||
totalCards: 1,
|
||||
totalWordsSeen: 350,
|
||||
totalTokensSeen: 350,
|
||||
totalYomitanLookupCount: 7,
|
||||
lastWatchedMs: 0,
|
||||
},
|
||||
@@ -102,7 +102,7 @@ test('EpisodeList renders per-episode Yomitan lookup rate', () => {
|
||||
);
|
||||
|
||||
assert.match(markup, /Lookup Rate/);
|
||||
assert.match(markup, /2\.0 \/ 100 words/);
|
||||
assert.match(markup, /2\.0 \/ 100 tokens/);
|
||||
});
|
||||
|
||||
test('AnimeOverviewStats renders aggregate Yomitan lookup metrics', () => {
|
||||
@@ -119,7 +119,7 @@ test('AnimeOverviewStats renders aggregate Yomitan lookup metrics', () => {
|
||||
totalSessions: 5,
|
||||
totalActiveMs: 100_000,
|
||||
totalCards: 8,
|
||||
totalWordsSeen: 800,
|
||||
totalTokensSeen: 800,
|
||||
totalLinesSeen: 100,
|
||||
totalLookupCount: 50,
|
||||
totalLookupHits: 30,
|
||||
@@ -134,8 +134,8 @@ test('AnimeOverviewStats renders aggregate Yomitan lookup metrics', () => {
|
||||
|
||||
assert.match(markup, /Lookups/);
|
||||
assert.match(markup, /16/);
|
||||
assert.match(markup, /2\.0 \/ 100 words/);
|
||||
assert.match(markup, /2\.0 lookups per 100 words/);
|
||||
assert.match(markup, /2\.0 \/ 100 tokens/);
|
||||
assert.match(markup, /Yomitan lookups per 100 tokens seen/);
|
||||
});
|
||||
|
||||
test('SessionRow prefers token-based word count when available', () => {
|
||||
@@ -152,7 +152,6 @@ test('SessionRow prefers token-based word count when available', () => {
|
||||
totalWatchedMs: 0,
|
||||
activeWatchedMs: 0,
|
||||
linesSeen: 12,
|
||||
wordsSeen: 12,
|
||||
tokensSeen: 42,
|
||||
cardsMined: 0,
|
||||
lookupCount: 0,
|
||||
|
||||
@@ -8,15 +8,15 @@ export interface LookupRateDisplay {
|
||||
|
||||
export function buildLookupRateDisplay(
|
||||
yomitanLookupCount: number,
|
||||
wordsSeen: number,
|
||||
tokensSeen: number,
|
||||
): LookupRateDisplay | null {
|
||||
if (!Number.isFinite(yomitanLookupCount) || !Number.isFinite(wordsSeen) || wordsSeen <= 0) {
|
||||
if (!Number.isFinite(yomitanLookupCount) || !Number.isFinite(tokensSeen) || tokensSeen <= 0) {
|
||||
return null;
|
||||
}
|
||||
const per100 = ((Math.max(0, yomitanLookupCount) / wordsSeen) * 100).toFixed(1);
|
||||
const per100 = ((Math.max(0, yomitanLookupCount) / tokensSeen) * 100).toFixed(1);
|
||||
return {
|
||||
shortValue: `${per100} / 100 words`,
|
||||
longValue: `${per100} lookups per 100 words`,
|
||||
shortValue: `${per100} / 100 tokens`,
|
||||
longValue: `${per100} lookups per 100 tokens`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user