@audiofool/moondog 0.2.0-beta.2 → 0.2.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/package.json +1 -1
- package/src/core/apple-projection-domain-services.mjs +95 -27
- package/src/core/capability-catalog.mjs +12 -0
- package/src/core/listener-avoids.mjs +101 -0
- package/src/core/listening-profile-domain-services.mjs +44 -8
- package/src/core/moondog-application.mjs +238 -34
- package/src/core/spotify-listening-intent.mjs +5 -2
- package/src/core/track-title.mjs +107 -0
- package/src/i18n/messages/de.mjs +7 -0
- package/src/i18n/messages/en.mjs +7 -0
- package/src/i18n/messages/es.mjs +7 -0
- package/src/i18n/messages/fr.mjs +7 -0
- package/src/i18n/messages/it.mjs +7 -0
- package/src/i18n/messages/ja.mjs +7 -0
- package/src/i18n/messages/ko.mjs +7 -0
- package/src/i18n/messages/pt.mjs +7 -0
- package/src/i18n/messages/zh-Hant.mjs +7 -0
- package/src/i18n/messages/zh.mjs +7 -0
- package/src/i18n/screen/es.mjs +1 -0
- package/src/i18n/screen/ja.mjs +1 -0
- package/src/i18n/screen/pt.mjs +1 -0
- package/src/i18n/screen/zh.mjs +1 -0
- package/src/integrations/apple-music/catalog.mjs +52 -9
- package/src/integrations/netease/ncm-cli-player.mjs +10 -3
- package/src/integrations/spotify/catalog-resolver.mjs +1 -2
- package/src/integrations/spotify/web-api-client.mjs +3 -0
- package/src/memory/local-memory-store.mjs +73 -16
- package/src/profile/listener-corrections.mjs +22 -0
- package/src/profile/listener-profile-build.mjs +92 -9
- package/src/profile/listener-profile-worker.mjs +7 -0
- package/src/profile/listening-history-store.mjs +231 -24
- package/src/profile/listening-profile-projection.mjs +212 -10
- package/src/profile/moondog-suggestions.mjs +184 -0
- package/src/profile/profile-exploration.mjs +16 -8
- package/src/runtime/pi/agent-runtime.mjs +363 -118
- package/src/runtime/pi/profile-build-runtime.mjs +2 -1
- package/src/runtime/pi/skills/listener-profile/SKILL.md +2 -0
- package/src/surfaces/cli/terminal-art.mjs +38 -16
- package/src/surfaces/cli/web-command.mjs +1 -1
|
@@ -36,6 +36,7 @@ const WRITER = [
|
|
|
36
36
|
"Plays, minutes and skips show attention, not love or dislike. Sparse years mean missing records, not silence. Apple snapshot play counts are a separate measure; never add them to dated plays. Treat every genre and format, including remixes and soundtracks, with the same respect.",
|
|
37
37
|
"Never invent explicit preferences, moods, activities, places, life events, reasons for listening, or listening on services that were not imported. The host keeps the listener's explicit choices separately; respect every current Avoid.",
|
|
38
38
|
"Cover the shape of the whole listening: the main threads, smaller but sustained interests, curated music the listener rarely plays, and recent movement. A profile of only the leading artists is incomplete.",
|
|
39
|
+
"Trajectory labels (rising, steady, fading, returning, burst) are deterministic observations of an artist's yearly plays. Use them to describe movement, such as an interest that faded or one that came back, with the yearly plays as support. A fading label is never dislike or a reason; say only that the plays declined.",
|
|
39
40
|
"Curated but rarely played music shows what the library holds and where to explore. Saves can come in batches or from tools, so do not present it as an established interest.",
|
|
40
41
|
"A count, time or skip the sources did not record is unknown, not zero or low: never call music unplayed, rarely played, new or unfamiliar to the listener only because no count was recorded.",
|
|
41
42
|
"Write to the listener as \"you\", in plain words. The summary has three to six sentences and leads with the music. Write five to ten insights, each one clear point in at most three sentences. Use plain hyphens, never em dashes.",
|
|
@@ -43,7 +44,7 @@ const WRITER = [
|
|
|
43
44
|
|
|
44
45
|
const CHECKER = [
|
|
45
46
|
"You check a draft listening profile in a fresh context. Judge the whole profile, not each citation.",
|
|
46
|
-
"Return revise only for a material problem: a contradiction between the summary and an insight or between insights; a wrong headline fact, such as a misidentified leader, period or scale, judged against the dossier or evidence; an invented explicit preference, personal situation or listening service; an unrecorded count, time or skip read as zero, low or unfamiliar; recent attention presented as replacing enduring taste, or attention presented as love; or a major thread clearly visible in the dossier's leading listening that the profile omits.",
|
|
47
|
+
"Return revise only for a material problem: a contradiction between the summary and an insight or between insights; a wrong headline fact, such as a misidentified leader, period or scale, judged against the dossier or evidence; an invented explicit preference, personal situation or listening service; an unrecorded count, time or skip read as zero, low or unfamiliar; recent attention presented as replacing enduring taste, or attention presented as love; an artist labelled fading described as a current interest when it is not among the dossier's recent artists, or one labelled returning described as new; or a major thread clearly visible in the dossier's leading listening that the profile omits.",
|
|
47
48
|
"Do not flag citation completeness, small numeric imprecision, phrasing, style or minor omissions. A plausible genre, scene or lineage interpretation labeled as a hypothesis is acceptable.",
|
|
48
49
|
"Return pass with no issues when nothing material is wrong. Otherwise return revise with at most six issues. Each names its target (summary or a claim_id), the problem and the correction.",
|
|
49
50
|
];
|
|
@@ -8,6 +8,7 @@ Do not present an unsaved conversational reading as a durable profile.
|
|
|
8
8
|
Use `moondog_profile_saved` to recover saved findings or inspect a claim and its evidence after a restart.
|
|
9
9
|
For fresh investigation or explanations beyond the saved reading, start with `moondog_profile_explore` using `section: overview`.
|
|
10
10
|
A stale saved profile reflects earlier evidence and must not be used as current taste.
|
|
11
|
+
A stale reading marked `previous_reading` is shown only as background: `listener_choices` and `choices_since_reading` are current and win wherever they differ from it.
|
|
11
12
|
A reading marked `current_through`, or stale with `newer_listening_only`, gained only new plays since it was built: it still holds as taste up to its last listening date, and newer listening comes from the history, not from the reading.
|
|
12
13
|
Current explicit choices always override inferred interests; historical listening does not make an avoided subject eligible for recommendations.
|
|
13
14
|
Report the actual source counts, date ranges, exclusions, and missing sources before describing taste.
|
|
@@ -29,6 +30,7 @@ Separate explicit listener choices, provider preference states, historical atten
|
|
|
29
30
|
Direct listener corrections take precedence, including Avoids outside the first page.
|
|
30
31
|
Form provisional conclusions, then use additional evidence to challenge the important ones.
|
|
31
32
|
Compare older and recent attention, inspect less-played tracks with `sort: least_played`, and check whether saved or positively rated music supports a different interest from the most-played music.
|
|
33
|
+
To see how taste moved across years, read the summary's movement or explore `history_artists` with `trajectory: fading` or `trajectory: returning`; a trajectory describes yearly plays as of the latest stored listening, not liking.
|
|
32
34
|
Use `moondog_profile_explain` for key evidence IDs and preserve conflicting evidence rather than forcing one persona.
|
|
33
35
|
Attach each important conclusion to its evidence IDs, source family, observed period, and a plain-language reason for certainty or uncertainty.
|
|
34
36
|
Do not manufacture a calibrated probability from a model's confidence.
|
|
@@ -198,8 +198,26 @@ export function lunarRecordShapes() {
|
|
|
198
198
|
return structuredClone({ dog, eye, ...shapes, earRadius: 0.17, grooves: [1, 0.91, 0.8, 0.66] });
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// The tonearm swings about its pivot, the arm's first point; a negative swing turns it off the record.
|
|
202
|
+
// Only an arm at rest on the record touches a groove, where its contact pulses as the record turns.
|
|
203
|
+
function paintTonearm({ pixel, disc, line, polygon }, rotation, swing = 0) {
|
|
204
|
+
const [pivotX, pivotY] = shapes.arm[0];
|
|
205
|
+
const [cosine, sine] = [Math.cos(swing), Math.sin(swing)];
|
|
206
|
+
const swung = ([x, y]) => [pivotX + (x - pivotX) * cosine - (y - pivotY) * sine, pivotY + (x - pivotX) * sine + (y - pivotY) * cosine];
|
|
207
|
+
const cartridge = shapes.cartridge.map(swung);
|
|
208
|
+
line(shapes.arm.map(swung), Math.max(pixel * 1.8, 0.03));
|
|
209
|
+
polygon(cartridge, true);
|
|
210
|
+
line([...cartridge, cartridge[0]], pixel);
|
|
211
|
+
disc(...swung([0.914, -0.764]), Math.max(pixel * 0.7, 0.021), false);
|
|
212
|
+
line([[0.775, -0.615], [0.735, -0.559]].map(swung), pixel * 1.3);
|
|
213
|
+
if (swing) return;
|
|
214
|
+
const pulse = Math.sin(rotation * 4);
|
|
215
|
+
const angle = -0.612 + pulse * pixel * 0.85;
|
|
216
|
+
disc(Math.cos(angle) * 0.928, Math.sin(angle) * 0.928, pixel * (0.95 + pulse * 0.15));
|
|
217
|
+
}
|
|
218
|
+
|
|
201
219
|
// The record as a field of dots, two across and four down per terminal cell, each marked with its layer.
|
|
202
|
-
function paint(columns, rows, phase, cellAspect) {
|
|
220
|
+
function paint(columns, rows, phase, cellAspect, tonearm = true) {
|
|
203
221
|
const art = drawing(columns, rows, cellAspect);
|
|
204
222
|
const { radius, pixel, circle, disc, line, polygon, paintLayer } = art;
|
|
205
223
|
const detail = radius >= 39 ? 2 : radius >= 27 ? 1 : 0;
|
|
@@ -243,7 +261,7 @@ function paint(columns, rows, phase, cellAspect) {
|
|
|
243
261
|
|
|
244
262
|
paintLayer(RECORD_LAYERS.figure);
|
|
245
263
|
// Like the logo, a clear halo separates the dog from the grooves around it.
|
|
246
|
-
const { headband, ear, nose
|
|
264
|
+
const { headband, ear, nose } = shapes;
|
|
247
265
|
line([...dog, dog[0]], pixel * 4.5, false);
|
|
248
266
|
line(headband, Math.max(pixel * 7, 0.11), false);
|
|
249
267
|
polygon(dog, false);
|
|
@@ -263,16 +281,7 @@ function paint(columns, rows, phase, cellAspect) {
|
|
|
263
281
|
disc(eye[0], eye[1], pixel * 1.05, false);
|
|
264
282
|
disc(nose[0], nose[1], pixel * 0.85);
|
|
265
283
|
|
|
266
|
-
|
|
267
|
-
polygon(cartridge, true);
|
|
268
|
-
line([...cartridge, cartridge[0]], pixel);
|
|
269
|
-
disc(0.914, -0.764, Math.max(pixel * 0.7, 0.021), false);
|
|
270
|
-
line([[0.775, -0.615], [0.735, -0.559]], pixel * 1.3);
|
|
271
|
-
const pulse = Math.sin(rotation * 4);
|
|
272
|
-
const angle = -0.612 + pulse * pixel * 0.85;
|
|
273
|
-
const contact = [Math.cos(angle) * 0.928, Math.sin(angle) * 0.928];
|
|
274
|
-
disc(contact[0], contact[1], pixel * (0.95 + pulse * 0.15));
|
|
275
|
-
|
|
284
|
+
if (tonearm) paintTonearm(art, rotation);
|
|
276
285
|
return art;
|
|
277
286
|
}
|
|
278
287
|
|
|
@@ -372,12 +381,25 @@ export function renderLunarRecordLayers({ columns = 56, rows = 24, style = "brai
|
|
|
372
381
|
/**
|
|
373
382
|
* The same record as dots rather than Braille characters, for surfaces that draw their own dots,
|
|
374
383
|
* such as the desktop window's canvas. Dots sit on a square grid of width x height, row by row;
|
|
375
|
-
* each holds 0 or the RECORD_LAYERS value that inked it.
|
|
384
|
+
* each holds 0 or the RECORD_LAYERS value that inked it. With tonearm: false the arm is left out, for
|
|
385
|
+
* a surface that draws it apart with renderTonearmDots so it can lift. centerX, centerY and radius
|
|
386
|
+
* place the record's unit coordinates on the grid, in dots.
|
|
376
387
|
*/
|
|
377
|
-
export function renderLunarRecordDots({ columns = 56, rows = 28, phase = 0 } = {}) {
|
|
388
|
+
export function renderLunarRecordDots({ columns = 56, rows = 28, phase = 0, tonearm = true } = {}) {
|
|
378
389
|
const frame = Number.isFinite(phase) ? ((Math.floor(phase) % LOGO_MOTION_FRAMES) + LOGO_MOTION_FRAMES) % LOGO_MOTION_FRAMES : 0;
|
|
379
|
-
const { dots, width, height } = paint(Math.max(10, Math.floor(columns)), Math.max(5, Math.floor(rows)), frame, 2);
|
|
380
|
-
return { dots, width, height };
|
|
390
|
+
const { dots, width, height, centerX, centerY, radius } = paint(Math.max(10, Math.floor(columns)), Math.max(5, Math.floor(rows)), frame, 2, tonearm);
|
|
391
|
+
return { dots, width, height, centerX, centerY, radius };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The tonearm alone, on the same grid as renderLunarRecordDots, swung `swing` radians about its pivot
|
|
396
|
+
* (negative lifts it off the record). At rest on the record its contact follows the record's phase.
|
|
397
|
+
*/
|
|
398
|
+
export function renderTonearmDots({ columns = 56, rows = 28, phase = 0, swing = 0 } = {}) {
|
|
399
|
+
const art = drawing(Math.max(10, Math.floor(columns)), Math.max(5, Math.floor(rows)), 2);
|
|
400
|
+
art.paintLayer(RECORD_LAYERS.figure);
|
|
401
|
+
paintTonearm(art, (Number.isFinite(phase) ? phase : 0) / LOGO_MOTION_FRAMES * turn, Number.isFinite(swing) ? swing : 0);
|
|
402
|
+
return { dots: art.dots, width: art.width, height: art.height };
|
|
381
403
|
}
|
|
382
404
|
|
|
383
405
|
/** Five-pixel letterforms, packed into three terminal rows without color or escapes. */
|
|
@@ -20,7 +20,7 @@ export function formatWebSources(sources, t = translator("en")) {
|
|
|
20
20
|
...sources.map((source) => [
|
|
21
21
|
`- ${link(source)}`,
|
|
22
22
|
...(source.published_at ? [t("web.sources.published", { date: inline(source.published_at) })] : []),
|
|
23
|
-
...(source.retrieved_at ? [t("web.sources.retrieved", { date: source.retrieved_at })] : []),
|
|
23
|
+
...(source.retrieved_at ? [t("web.sources.retrieved", { date: source.retrieved_at.slice(0, 10) })] : []),
|
|
24
24
|
].join(" - ")),
|
|
25
25
|
].join("\n");
|
|
26
26
|
}
|