cotutor 0.3.0 → 0.4.0
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/LICENSE +9 -7
- package/NOTICE +3 -3
- package/README.md +36 -13
- package/avatars/chinese.webp +0 -0
- package/avatars/english.webp +0 -0
- package/avatars/math.webp +0 -0
- package/bin/cotutor.js +13 -3
- package/cards/photo/card.css +10 -3
- package/cards/photo/card.md +2 -2
- package/cards/tutor/card.css +13 -0
- package/cards/tutor/card.md +2 -1
- package/cards//346/235/277/344/271/246/346/200/216/344/271/210/345/206/231.md +1 -1
- package/dist/app/activity-eval.js +3 -2
- package/dist/app/cli/activities.d.ts +8 -1
- package/dist/app/cli/activities.js +42 -4
- package/dist/app/cli/cert.d.ts +5 -0
- package/dist/app/cli/cert.js +20 -7
- package/dist/app/cli/chalk/browser.d.ts +2 -2
- package/dist/app/cli/chalk/browser.js +22 -7
- package/dist/app/cli/chalk/main.js +3 -2
- package/dist/app/cli/doctor.d.ts +6 -5
- package/dist/app/cli/doctor.js +107 -171
- package/dist/app/cli/firewall.d.ts +24 -0
- package/dist/app/cli/firewall.js +79 -0
- package/dist/app/cli/init.js +8 -5
- package/dist/app/cli/main.js +347 -24
- package/dist/app/cli/migrate.d.ts +2 -11
- package/dist/app/cli/migrate.js +1 -59
- package/dist/app/cli/port-holder.d.ts +16 -0
- package/dist/app/cli/port-holder.js +34 -0
- package/dist/app/cli/port.d.ts +37 -0
- package/dist/app/cli/port.js +116 -0
- package/dist/app/cli/sandbox.d.ts +25 -0
- package/dist/app/cli/sandbox.js +166 -0
- package/dist/app/cli/serve-log.d.ts +13 -0
- package/dist/app/cli/serve-log.js +90 -0
- package/dist/app/cli/serve.d.ts +18 -4
- package/dist/app/cli/serve.js +78 -8
- package/dist/app/cli/service-windows.d.ts +50 -0
- package/dist/app/cli/service-windows.js +117 -0
- package/dist/app/cli/service.d.ts +138 -0
- package/dist/app/cli/service.js +429 -0
- package/dist/app/cli/skeleton.d.ts +1 -1
- package/dist/app/cli/skeleton.js +13 -13
- package/dist/app/cli/skills.d.ts +5 -1
- package/dist/app/cli/skills.js +14 -4
- package/dist/app/cli/tray-icons.d.ts +11 -0
- package/dist/app/cli/tray-icons.js +87 -0
- package/dist/app/cli/tray-ids.d.ts +6 -0
- package/dist/app/cli/tray-ids.js +6 -0
- package/dist/app/cli/tray-mac.d.ts +8 -0
- package/dist/app/cli/tray-mac.js +140 -0
- package/dist/app/cli/tray-run.d.ts +1 -0
- package/dist/app/cli/tray-run.js +33 -0
- package/dist/app/cli/tray-windows.d.ts +5 -0
- package/dist/app/cli/tray-windows.js +166 -0
- package/dist/app/cli/tray.d.ts +205 -0
- package/dist/app/cli/tray.js +618 -0
- package/dist/app/cli/usage.d.ts +1 -1
- package/dist/app/cli/usage.js +11 -6
- package/dist/app/dictation/route.js +3 -2
- package/dist/app/http/app.d.ts +5 -10
- package/dist/app/http/app.js +45 -98
- package/dist/app/http/avatars.d.ts +17 -0
- package/dist/app/http/avatars.js +35 -0
- package/dist/app/http/dev-tutor-files.js +1 -1
- package/dist/app/http/parent-home.d.ts +14 -0
- package/dist/app/http/parent-home.js +69 -6
- package/dist/app/http/parent-look.d.ts +2 -0
- package/dist/app/http/parent-look.js +151 -0
- package/dist/app/http/raw-view.js +3 -3
- package/dist/app/http/voice-preview.d.ts +4 -0
- package/dist/app/http/voice-preview.js +34 -0
- package/dist/app/machine/bailian.d.ts +0 -2
- package/dist/app/machine/bailian.js +5 -6
- package/dist/app/mock/mock-live.js +1 -1
- package/dist/app/mock/mock.d.ts +4 -2
- package/dist/app/mock/mock.js +20 -12
- package/dist/app/replay.js +4 -4
- package/dist/app/skills/home-doc.js +1 -1
- package/dist/app/workspace.d.ts +9 -4
- package/dist/app/workspace.js +33 -10
- package/dist/app/ws/route.d.ts +8 -2
- package/dist/app/ws/route.js +7 -3
- package/dist/board/parse.d.ts +0 -2
- package/dist/board/parse.js +0 -6
- package/dist/board/presentation.d.ts +9 -15
- package/dist/board/presentation.js +8 -10
- package/dist/brain/api-tools.js +2 -1
- package/dist/brain/api.d.ts +12 -17
- package/dist/brain/api.js +16 -33
- package/dist/brain/index.d.ts +23 -71
- package/dist/brain/index.js +6 -69
- package/dist/brain/log.d.ts +5 -0
- package/dist/brain/log.js +22 -0
- package/dist/brain/runtime.d.ts +30 -0
- package/dist/brain/runtime.js +23 -0
- package/dist/brain/{cli/stream-json.d.ts → stream-json.d.ts} +2 -4
- package/dist/brain/{cli/stream-json.js → stream-json.js} +0 -4
- package/dist/brain/stream.d.ts +5 -5
- package/dist/brain/stream.js +7 -7
- package/dist/brain/transcript.d.ts +3 -3
- package/dist/brain/transcript.js +6 -6
- package/dist/brain/{cli/types.d.ts → types.d.ts} +3 -48
- package/dist/brain/types.js +14 -0
- package/dist/chalk/glyphs/skeleton.d.ts +3 -3
- package/dist/chalk/glyphs/skeleton.js +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/memory/diary.js +3 -3
- package/dist/realtime/bailian.d.ts +1 -1
- package/dist/realtime/bailian.js +3 -2
- package/dist/realtime/preview.js +1 -1
- package/dist/realtime/voices.d.ts +2 -0
- package/dist/realtime/voices.js +57 -57
- package/dist/runtime/activities.d.ts +13 -1
- package/dist/runtime/activities.js +28 -2
- package/dist/runtime/activity-state.js +2 -2
- package/dist/runtime/agent-file.d.ts +3 -3
- package/dist/runtime/agent-file.js +3 -3
- package/dist/runtime/context/tutor-rules.d.ts +2 -2
- package/dist/runtime/context/tutor-rules.js +3 -3
- package/dist/runtime/home/home.d.ts +12 -1
- package/dist/runtime/home/home.js +8 -1
- package/dist/runtime/home/store.d.ts +1 -1
- package/dist/runtime/home/store.js +36 -21
- package/dist/runtime/journal/mix.d.ts +15 -6
- package/dist/runtime/journal/mix.js +60 -19
- package/dist/runtime/journal/recorder.d.ts +58 -13
- package/dist/runtime/journal/recorder.js +106 -16
- package/dist/runtime/journal/store.d.ts +4 -4
- package/dist/runtime/journal/store.js +6 -6
- package/dist/runtime/kid-input.js +2 -3
- package/dist/runtime/live/call.d.ts +12 -0
- package/dist/runtime/live/call.js +51 -18
- package/dist/runtime/live/turns.js +1 -1
- package/dist/runtime/runner.d.ts +9 -55
- package/dist/runtime/runner.js +52 -173
- package/dist/scenes/paint/painter.js +4 -3
- package/dist/schema/config.d.ts +43 -22
- package/dist/schema/config.js +45 -47
- package/dist/schema/conversation.d.ts +6 -0
- package/dist/schema/home.d.ts +11 -1
- package/dist/schema/home.js +7 -2
- package/dist/schema/workspace.d.ts +27 -0
- package/dist/schema/workspace.js +28 -0
- package/dist/shared/default-port.d.ts +2 -0
- package/dist/shared/default-port.js +2 -0
- package/dist/shared/qianwen.d.ts +14 -10
- package/dist/shared/qianwen.js +26 -22
- package/dist/shared/site-url.d.ts +2 -0
- package/dist/shared/site-url.js +5 -0
- package/dist/ui/dev/dev-page.js +25 -252
- package/dist/ui/dev/dev-theme.d.ts +1 -1
- package/dist/ui/dev/dev-theme.js +1 -1
- package/dist/ui/dev/voice-gallery-page.js +1 -1
- package/dist/ui/kid/board-mark.d.ts +2 -2
- package/dist/ui/kid/board-mark.js +2 -2
- package/dist/ui/kid/home-tiles.d.ts +14 -0
- package/dist/ui/kid/home-tiles.js +44 -0
- package/dist/ui/kid/kid-page.js +68 -103
- package/dist/ui/kid/voice-ui.d.ts +1 -1
- package/dist/ui/kid/voice-ui.js +1 -1
- package/dist/ui/kid/word-emoji.js +5 -3
- package/dist/ui/parent/parent-home-page.js +315 -7
- package/dist/ui/parent/parent-page.js +6 -2
- package/dist/ui/parent/parent-settings-page.d.ts +1 -0
- package/dist/ui/parent/parent-settings-page.js +133 -0
- package/dist/ui/{dev → parent}/qr-page.d.ts +2 -2
- package/dist/ui/{dev → parent}/qr-page.js +19 -14
- package/dist/ui/shared/brand.d.ts +3 -1
- package/dist/ui/shared/brand.js +3 -1
- package/grades/grade-2/README.md +1 -1
- package/grades/grade-2/VALIDATION.md +1 -1
- package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240.md +1 -1
- package/grades/grade-2/agents/chinese-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
- package/grades/grade-2/agents/chinese-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
- package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240.md +1 -1
- package/grades/grade-2/agents/english-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
- package/grades/grade-2/agents/english-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
- package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240.md +1 -1
- package/grades/grade-2/agents/math-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
- package/grades/grade-2/agents/math-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
- package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/package.json +2 -1
- package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +2 -2
- package/skills/cotutor-board/references/photo.md +2 -2
- package/skills/cotutor-home/SKILL.md +1 -0
- package/skills/cotutor-home/references/tutor.md +2 -1
- package/skills/cotutor-home/references//351/246/226/351/241/265/350/257/255/346/263/225.md +2 -1
- package/skills/cotutor-tune/references//345/255/227/346/256/265.md +6 -3
- package/skills/cotutor-tutor/SKILL.md +5 -13
- package/dist/brain/cli/adapters.d.ts +0 -60
- package/dist/brain/cli/adapters.js +0 -91
- package/dist/brain/cli/qwen.d.ts +0 -11
- package/dist/brain/cli/qwen.js +0 -141
- package/dist/brain/cli/run-plan.d.ts +0 -59
- package/dist/brain/cli/run-plan.js +0 -71
- package/dist/brain/cli/run.d.ts +0 -35
- package/dist/brain/cli/run.js +0 -256
- package/dist/brain/cli/types.js +0 -3
- package/dist/brain/cli/warm.d.ts +0 -53
- package/dist/brain/cli/warm.js +0 -103
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一次通话的录像记录(录数据,不录屏幕):口语课与实时老师共用。
|
|
3
|
+
* 发给孩子端(o)/ 孩子端发来(i)的每条消息带离开始的毫秒;两边的声音每块带到达的毫秒;孩子端清播放队列的时刻;屏幕多大。
|
|
4
|
+
* 按段收(2026-11-06):页面开着不关,一通电话能挂几个钟头,声音全攒在内存里挂断时再混,一个钟头就上 G。
|
|
5
|
+
* cut() 收下这一段、接着录新的一段;什么时候切由调用方定(call.ts:两轮之间安静着、没动静够久了或这一段够长了)。
|
|
6
|
+
* ms() 一直是离通话开始的毫秒(调用方的时钟,切段不动它);收下的那一段里的时刻都换成离这一段开始的毫秒。
|
|
7
|
+
* 一段尾巴上没动静的那截不要(TAKE_TAIL_MS 之后);没动静的一段收下来 stirred = false,调用方丢掉;
|
|
8
|
+
* 没动静的一段第一次有动静时,TAKE_LEAD_MS 之前的那截丢掉(孩子走开一个钟头再回来,录像不从一个钟头前开始)。
|
|
9
|
+
* 收下的(RawTake)交给 renderTake:两边按到达时刻混成一条(src/runtime/journal/mix.ts,分片混、不卡事件循环),给出录像的数据(reel.json 的形状)与要落盘的声音;落在哪由调用方定。
|
|
10
|
+
*/
|
|
11
|
+
import { type Chunk } from './mix.ts';
|
|
1
12
|
export type ReelMsg = [number, 'o' | 'i', Record<string, unknown>];
|
|
2
|
-
/** reel.json
|
|
13
|
+
/** reel.json:消息、老师出声的时段、孩子有声的时段、屏幕多大;声音另在同名的 .wav(24k 单声道) */
|
|
3
14
|
export interface CallReel {
|
|
4
15
|
v: 1;
|
|
5
16
|
startedAt: string;
|
|
@@ -9,38 +20,72 @@ export interface CallReel {
|
|
|
9
20
|
playing: [number, number][];
|
|
10
21
|
kidVoice: [number, number][];
|
|
11
22
|
}
|
|
23
|
+
/** 收下的一段,还没混:时刻都是离这一段开始的毫秒(开始之前到的块是负的,混的时候落在外面的不要) */
|
|
24
|
+
export interface RawTake {
|
|
25
|
+
startedAt: Date;
|
|
26
|
+
ms: number;
|
|
27
|
+
view: [number, number] | null;
|
|
28
|
+
msgs: ReelMsg[];
|
|
29
|
+
kid: Chunk[];
|
|
30
|
+
tutor: Chunk[];
|
|
31
|
+
flushes: number[];
|
|
32
|
+
/** 这一段有没有动静(孩子按下、发来、嘴说了……);没有的不落 */
|
|
33
|
+
stirred: boolean;
|
|
34
|
+
}
|
|
12
35
|
export interface CallTake {
|
|
13
36
|
ms: number;
|
|
14
37
|
reel: CallReel;
|
|
15
38
|
/** 两边按时刻混好的一条(MIX_RATE) */
|
|
16
39
|
callPcm: Buffer;
|
|
17
|
-
/** 两边各自首尾相接(孩子 16k、老师 24k),没有时刻 */
|
|
18
|
-
kidPcm: Buffer;
|
|
19
|
-
tutorPcm: Buffer;
|
|
20
40
|
}
|
|
41
|
+
/** 一段尾巴上:最后一次动静之后留这么久 */
|
|
42
|
+
export declare const TAKE_TAIL_MS = 2000;
|
|
43
|
+
/** 一段开头:第一次动静之前留这么久 */
|
|
44
|
+
export declare const TAKE_LEAD_MS = 2000;
|
|
21
45
|
export declare class CallRecorder {
|
|
22
46
|
readonly startedAt: Date;
|
|
23
47
|
private readonly now;
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
private
|
|
48
|
+
private msgs;
|
|
49
|
+
private kidChunks;
|
|
50
|
+
private tutorChunks;
|
|
51
|
+
private flushes;
|
|
28
52
|
private size;
|
|
53
|
+
/** 这一段从哪一刻起(离通话开始的毫秒) */
|
|
54
|
+
private base;
|
|
55
|
+
/** 这一段最后一次动静(老师的声音算到它在孩子端放完);没动静时 = base */
|
|
56
|
+
private lastStir;
|
|
57
|
+
private stirredNow;
|
|
58
|
+
/** 老师的声音在孩子端排到哪(估的,flush 不截;只用来算最后一次动静) */
|
|
59
|
+
private tutorEnd;
|
|
29
60
|
constructor(now?: () => Date);
|
|
30
|
-
/**
|
|
61
|
+
/** 离通话开始多少毫秒(切段不动它) */
|
|
31
62
|
ms(): number;
|
|
63
|
+
/** 这一段多久没动静了 */
|
|
64
|
+
quietMs(): number;
|
|
65
|
+
/** 这一段录了多久 */
|
|
66
|
+
takeMs(): number;
|
|
67
|
+
/** 这一段有过动静没有 */
|
|
68
|
+
get stirred(): boolean;
|
|
32
69
|
/** 发给孩子端的一条;{t:'flush'} 同时记下清队列的时刻 */
|
|
33
70
|
out(m: Record<string, unknown>): void;
|
|
34
71
|
/** 孩子端发来的一条 */
|
|
35
72
|
in(m: Record<string, unknown>, t?: number): void;
|
|
36
73
|
/** 孩子端清了播放队列,但没有一条 flush 消息(比如换段时页面自己清) */
|
|
37
74
|
flush(): void;
|
|
38
|
-
/** 孩子的一块声音(16k);t 缺省现在(开口前垫上的那几块带自己到的时刻) */
|
|
75
|
+
/** 孩子的一块声音(16k);t 缺省现在(开口前垫上的那几块带自己到的时刻)。不算动静:一直听时没人说话也一直在推 */
|
|
39
76
|
kid(pcm: Buffer, t?: number): void;
|
|
40
|
-
/** 老师的一块声音(24k) */
|
|
77
|
+
/** 老师的一块声音(24k):算动静,算到它在孩子端放完 */
|
|
41
78
|
tutor(pcm: Buffer): void;
|
|
42
79
|
/** 孩子端报的屏幕尺寸;不像样的不记 */
|
|
43
80
|
view(w: unknown, h: unknown): void;
|
|
44
|
-
/**
|
|
45
|
-
|
|
81
|
+
/** 收这一段,从现在起录新的一段(调用方在两轮之间安静时切) */
|
|
82
|
+
cut(): RawTake;
|
|
83
|
+
/** 挂了:收最后一段(ms 缺省现在) */
|
|
84
|
+
finish(ms?: number): RawTake;
|
|
85
|
+
/** 有动静:这一段头一次有动静就把 TAKE_LEAD_MS 之前的丢掉 */
|
|
86
|
+
private stir;
|
|
87
|
+
/** 这一段到 end 为止(有动静的截到最后一次动静 + TAKE_TAIL_MS),时刻换成离这一段开始的毫秒 */
|
|
88
|
+
private take;
|
|
46
89
|
}
|
|
90
|
+
/** 混声音、拼录像的数据(分片混,不卡事件循环) */
|
|
91
|
+
export declare function renderTake(raw: RawTake): Promise<CallTake>;
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 一次通话的录像记录(录数据,不录屏幕):口语课与实时老师共用。
|
|
3
3
|
* 发给孩子端(o)/ 孩子端发来(i)的每条消息带离开始的毫秒;两边的声音每块带到达的毫秒;孩子端清播放队列的时刻;屏幕多大。
|
|
4
|
-
*
|
|
4
|
+
* 按段收(2026-11-06):页面开着不关,一通电话能挂几个钟头,声音全攒在内存里挂断时再混,一个钟头就上 G。
|
|
5
|
+
* cut() 收下这一段、接着录新的一段;什么时候切由调用方定(call.ts:两轮之间安静着、没动静够久了或这一段够长了)。
|
|
6
|
+
* ms() 一直是离通话开始的毫秒(调用方的时钟,切段不动它);收下的那一段里的时刻都换成离这一段开始的毫秒。
|
|
7
|
+
* 一段尾巴上没动静的那截不要(TAKE_TAIL_MS 之后);没动静的一段收下来 stirred = false,调用方丢掉;
|
|
8
|
+
* 没动静的一段第一次有动静时,TAKE_LEAD_MS 之前的那截丢掉(孩子走开一个钟头再回来,录像不从一个钟头前开始)。
|
|
9
|
+
* 收下的(RawTake)交给 renderTake:两边按到达时刻混成一条(src/runtime/journal/mix.ts,分片混、不卡事件循环),给出录像的数据(reel.json 的形状)与要落盘的声音;落在哪由调用方定。
|
|
5
10
|
*/
|
|
6
|
-
import {
|
|
11
|
+
import { mixCallSliced } from "./mix.js";
|
|
12
|
+
/** 一段尾巴上:最后一次动静之后留这么久 */
|
|
13
|
+
export const TAKE_TAIL_MS = 2000;
|
|
14
|
+
/** 一段开头:第一次动静之前留这么久 */
|
|
15
|
+
export const TAKE_LEAD_MS = 2000;
|
|
16
|
+
/** 不算动静的:孩子端报屏幕尺寸、说再见;嘴连上、换连接、断了、挂断(只记不发的那几条也在) */
|
|
17
|
+
const STILL_IN = new Set(['view', 'bye']);
|
|
18
|
+
const STILL_OUT = new Set(['ready', 'swap', 'lost', 'down', 'end']);
|
|
19
|
+
/** 孩子端排老师的声音起步(同 mix.ts 的 LEAD_MS):估这块放完的时刻用 */
|
|
20
|
+
const TUTOR_LEAD_MS = 60;
|
|
21
|
+
const TUTOR_RATE = 24000;
|
|
7
22
|
export class CallRecorder {
|
|
8
23
|
startedAt;
|
|
9
24
|
now;
|
|
@@ -12,21 +27,40 @@ export class CallRecorder {
|
|
|
12
27
|
tutorChunks = [];
|
|
13
28
|
flushes = [];
|
|
14
29
|
size = null;
|
|
30
|
+
/** 这一段从哪一刻起(离通话开始的毫秒) */
|
|
31
|
+
base = 0;
|
|
32
|
+
/** 这一段最后一次动静(老师的声音算到它在孩子端放完);没动静时 = base */
|
|
33
|
+
lastStir = 0;
|
|
34
|
+
stirredNow = false;
|
|
35
|
+
/** 老师的声音在孩子端排到哪(估的,flush 不截;只用来算最后一次动静) */
|
|
36
|
+
tutorEnd = 0;
|
|
15
37
|
constructor(now = () => new Date()) {
|
|
16
38
|
this.now = now;
|
|
17
39
|
this.startedAt = now();
|
|
18
40
|
}
|
|
19
|
-
/**
|
|
41
|
+
/** 离通话开始多少毫秒(切段不动它) */
|
|
20
42
|
ms() { return this.now().getTime() - this.startedAt.getTime(); }
|
|
43
|
+
/** 这一段多久没动静了 */
|
|
44
|
+
quietMs() { return this.ms() - this.lastStir; }
|
|
45
|
+
/** 这一段录了多久 */
|
|
46
|
+
takeMs() { return this.ms() - this.base; }
|
|
47
|
+
/** 这一段有过动静没有 */
|
|
48
|
+
get stirred() { return this.stirredNow; }
|
|
21
49
|
/** 发给孩子端的一条;{t:'flush'} 同时记下清队列的时刻 */
|
|
22
50
|
out(m) {
|
|
23
51
|
const t = this.ms();
|
|
24
52
|
this.msgs.push([t, 'o', m]);
|
|
25
|
-
if (m.t === 'flush')
|
|
53
|
+
if (m.t === 'flush') {
|
|
26
54
|
this.flushes.push(t);
|
|
55
|
+
this.tutorEnd = 0;
|
|
56
|
+
}
|
|
57
|
+
if (!STILL_OUT.has(String(m.t)))
|
|
58
|
+
this.stir(t);
|
|
27
59
|
}
|
|
28
60
|
/** 孩子端发来的一条 */
|
|
29
61
|
in(m, t = this.ms()) {
|
|
62
|
+
if (!STILL_IN.has(String(m.t)))
|
|
63
|
+
this.stir(t);
|
|
30
64
|
// 一直听时孩子那一句的开口 / 说完(补记的 hold / release)要记回它当时的时刻,按时刻插进去,录像按顺序读
|
|
31
65
|
let i = this.msgs.length;
|
|
32
66
|
while (i > 0 && this.msgs[i - 1][0] > t)
|
|
@@ -34,25 +68,81 @@ export class CallRecorder {
|
|
|
34
68
|
this.msgs.splice(i, 0, [t, 'i', m]);
|
|
35
69
|
}
|
|
36
70
|
/** 孩子端清了播放队列,但没有一条 flush 消息(比如换段时页面自己清) */
|
|
37
|
-
flush() {
|
|
38
|
-
|
|
71
|
+
flush() {
|
|
72
|
+
const t = this.ms();
|
|
73
|
+
this.flushes.push(t);
|
|
74
|
+
this.tutorEnd = 0;
|
|
75
|
+
this.stir(t);
|
|
76
|
+
}
|
|
77
|
+
/** 孩子的一块声音(16k);t 缺省现在(开口前垫上的那几块带自己到的时刻)。不算动静:一直听时没人说话也一直在推 */
|
|
39
78
|
kid(pcm, t = this.ms()) { this.kidChunks.push({ t, pcm }); }
|
|
40
|
-
/** 老师的一块声音(24k) */
|
|
41
|
-
tutor(pcm) {
|
|
79
|
+
/** 老师的一块声音(24k):算动静,算到它在孩子端放完 */
|
|
80
|
+
tutor(pcm) {
|
|
81
|
+
const t = this.ms();
|
|
82
|
+
this.tutorChunks.push({ t, pcm });
|
|
83
|
+
this.tutorEnd = Math.max(t + TUTOR_LEAD_MS, this.tutorEnd) + ((pcm.length >> 1) * 1000) / TUTOR_RATE;
|
|
84
|
+
this.stir(t);
|
|
85
|
+
this.lastStir = Math.max(this.lastStir, this.tutorEnd);
|
|
86
|
+
}
|
|
42
87
|
/** 孩子端报的屏幕尺寸;不像样的不记 */
|
|
43
88
|
view(w, h) {
|
|
44
89
|
if (typeof w === 'number' && typeof h === 'number' && w > 0 && h > 0)
|
|
45
90
|
this.size = [Math.round(w), Math.round(h)];
|
|
46
91
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
const
|
|
92
|
+
/** 收这一段,从现在起录新的一段(调用方在两轮之间安静时切) */
|
|
93
|
+
cut() {
|
|
94
|
+
const now = this.ms();
|
|
95
|
+
const raw = this.take(now);
|
|
96
|
+
this.msgs = [];
|
|
97
|
+
this.kidChunks = [];
|
|
98
|
+
this.tutorChunks = [];
|
|
99
|
+
this.flushes = [];
|
|
100
|
+
this.base = now;
|
|
101
|
+
this.lastStir = now;
|
|
102
|
+
this.stirredNow = false;
|
|
103
|
+
this.tutorEnd = 0;
|
|
104
|
+
return raw;
|
|
105
|
+
}
|
|
106
|
+
/** 挂了:收最后一段(ms 缺省现在) */
|
|
107
|
+
finish(ms = this.ms()) { return this.take(ms); }
|
|
108
|
+
/** 有动静:这一段头一次有动静就把 TAKE_LEAD_MS 之前的丢掉 */
|
|
109
|
+
stir(t) {
|
|
110
|
+
if (!this.stirredNow) {
|
|
111
|
+
this.stirredNow = true;
|
|
112
|
+
const from = t - TAKE_LEAD_MS;
|
|
113
|
+
if (from > this.base) {
|
|
114
|
+
this.base = from;
|
|
115
|
+
this.msgs = this.msgs.filter(([at]) => at >= from);
|
|
116
|
+
this.kidChunks = this.kidChunks.filter((c) => c.t > from);
|
|
117
|
+
this.tutorChunks = this.tutorChunks.filter((c) => c.t > from);
|
|
118
|
+
this.flushes = this.flushes.filter((at) => at >= from);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (t > this.lastStir)
|
|
122
|
+
this.lastStir = t;
|
|
123
|
+
}
|
|
124
|
+
/** 这一段到 end 为止(有动静的截到最后一次动静 + TAKE_TAIL_MS),时刻换成离这一段开始的毫秒 */
|
|
125
|
+
take(end) {
|
|
126
|
+
const b = this.base;
|
|
127
|
+
const stop = this.stirredNow ? Math.min(end, this.lastStir + TAKE_TAIL_MS) : end;
|
|
50
128
|
return {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
129
|
+
startedAt: new Date(this.startedAt.getTime() + b),
|
|
130
|
+
ms: Math.max(0, Math.round(stop - b)),
|
|
131
|
+
view: this.size,
|
|
132
|
+
msgs: this.msgs.map(([t, d, m]) => [Math.max(0, t - b), d, m]),
|
|
133
|
+
kid: this.kidChunks.map((c) => ({ t: c.t - b, pcm: c.pcm })),
|
|
134
|
+
tutor: this.tutorChunks.map((c) => ({ t: c.t - b, pcm: c.pcm })),
|
|
135
|
+
flushes: this.flushes.map((t) => t - b),
|
|
136
|
+
stirred: this.stirredNow,
|
|
56
137
|
};
|
|
57
138
|
}
|
|
58
139
|
}
|
|
140
|
+
/** 混声音、拼录像的数据(分片混,不卡事件循环) */
|
|
141
|
+
export async function renderTake(raw) {
|
|
142
|
+
const mix = await mixCallSliced({ kid: raw.kid, tutor: raw.tutor, flushes: raw.flushes, ms: raw.ms });
|
|
143
|
+
return {
|
|
144
|
+
ms: raw.ms,
|
|
145
|
+
reel: { v: 1, startedAt: raw.startedAt.toISOString(), ms: raw.ms, view: raw.view, msgs: raw.msgs, playing: mix.playing, kidVoice: mix.kidVoice },
|
|
146
|
+
callPcm: mix.pcm,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -11,8 +11,8 @@ export declare function readIndex(ws: Workspace, tutor: string, date: string): P
|
|
|
11
11
|
export declare function writeIndex(ws: Workspace, index: ConversationIndex): Promise<void>;
|
|
12
12
|
/** 有过对话的日期,新的在前 */
|
|
13
13
|
export declare function listDates(ws: Workspace, tutor: string): Promise<string[]>;
|
|
14
|
-
/**
|
|
15
|
-
export declare function readTranscript(ws: Workspace, tutor: string, date: string, job: string
|
|
14
|
+
/** 一轮的转录:统一事件,旧轮次的 stream-json 也读(src/brain/log.ts) */
|
|
15
|
+
export declare function readTranscript(ws: Workspace, tutor: string, date: string, job: string): Promise<Transcript | null>;
|
|
16
16
|
/**
|
|
17
17
|
* 一天里卡的目录 <date>.<job>.cards/ 全扫一遍:<n>.json 是孩子做的事(坏文件跳过,孩子端不报),<n>/<k>.mp3 是后台生成好的资产。
|
|
18
18
|
*/
|
|
@@ -72,7 +72,7 @@ export interface RunFile {
|
|
|
72
72
|
session: string | null;
|
|
73
73
|
/** 老师文件与技能的快照;2026-09-15 之前的轮次没有 */
|
|
74
74
|
sources?: RunSources;
|
|
75
|
-
/**
|
|
75
|
+
/** 老师正文进了系统提示(API 运行时总是;旧轮次看那时的命令模板) */
|
|
76
76
|
agentBody: boolean;
|
|
77
77
|
}
|
|
78
78
|
export declare function writeRunFile(ws: Workspace, tutor: string, date: string, job: string, r: {
|
|
@@ -91,7 +91,7 @@ export declare function readRunFile(ws: Workspace, tutor: string, date: string,
|
|
|
91
91
|
export declare function readErrLog(ws: Workspace, tutor: string, date: string, job: string): Promise<string>;
|
|
92
92
|
/** 这轮起跑时老师文件与技能长什么样(记进 run.json;读不到的就 null / 空,不拦这一轮) */
|
|
93
93
|
export declare function snapshotSources(ws: Workspace, name: string): Promise<RunSources>;
|
|
94
|
-
/** 老师文件正文(
|
|
94
|
+
/** 老师文件正文(系统提示的前半);从 agents/<name>.md 读 */
|
|
95
95
|
export declare function readAgentBody(ws: Workspace, name: string): Promise<string>;
|
|
96
96
|
/** 给一个话题打星(1–5;null 清掉):话题要在这天的索引里;写回索引 */
|
|
97
97
|
export declare function rateThread(ws: Workspace, tutor: string, date: string, thread: string, rating: number | null): Promise<ConversationIndex>;
|
|
@@ -10,7 +10,7 @@ import { emptyIndex } from "./conversation.js";
|
|
|
10
10
|
import { localDate } from "../../shared/time.js";
|
|
11
11
|
import { cardAssetName, conversationFiles, threads, tutorFilePath } from "../../schema/index.js";
|
|
12
12
|
import { parseTranscript } from "../../brain/transcript.js";
|
|
13
|
-
import {
|
|
13
|
+
import { parseLogLine } from "../../brain/index.js";
|
|
14
14
|
import { ConversationIndexSchema, DATE_RE, explainIssues, } from "../../schema/index.js";
|
|
15
15
|
import { undoThreadVault } from "../../memory/vault.js";
|
|
16
16
|
import { recoverPending } from "../../memory/commit.js";
|
|
@@ -64,10 +64,10 @@ export async function listDates(ws, tutor) {
|
|
|
64
64
|
return [];
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
68
|
-
export async function readTranscript(ws, tutor, date, job
|
|
67
|
+
/** 一轮的转录:统一事件,旧轮次的 stream-json 也读(src/brain/log.ts) */
|
|
68
|
+
export async function readTranscript(ws, tutor, date, job) {
|
|
69
69
|
try {
|
|
70
|
-
return parseTranscript(await readFile(conversationFiles(ws.dirs.conversations, tutor, date).log(job), 'utf8'),
|
|
70
|
+
return parseTranscript(await readFile(conversationFiles(ws.dirs.conversations, tutor, date).log(job), 'utf8'), parseLogLine);
|
|
71
71
|
}
|
|
72
72
|
catch {
|
|
73
73
|
return null;
|
|
@@ -209,7 +209,7 @@ export async function snapshotSources(ws, name) {
|
|
|
209
209
|
const file = tutorFilePath(ws.dirs.agents, name);
|
|
210
210
|
try {
|
|
211
211
|
const body = await readFile(file, 'utf8');
|
|
212
|
-
agent = { file: relative(ws.root, file), hash: sha(body), body };
|
|
212
|
+
agent = { file: relative(ws.root, file).split(sep).join('/'), hash: sha(body), body };
|
|
213
213
|
}
|
|
214
214
|
catch {
|
|
215
215
|
/* 读不到:记 null */
|
|
@@ -224,7 +224,7 @@ export async function snapshotSources(ws, name) {
|
|
|
224
224
|
}
|
|
225
225
|
return { agent, skills };
|
|
226
226
|
}
|
|
227
|
-
/** 老师文件正文(
|
|
227
|
+
/** 老师文件正文(系统提示的前半);从 agents/<name>.md 读 */
|
|
228
228
|
export async function readAgentBody(ws, name) {
|
|
229
229
|
const file = tutorFilePath(ws.dirs.agents, name);
|
|
230
230
|
try {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* 怎么告诉孩子端由各自的协议定(HTTP 回 JSON,WS 回 err)。不起老师。
|
|
2
|
+
* 孩子端发一条的验与拼:孩子端只走实时那条连接(src/runtime/live/turns.ts;HTTP 的发 2026-11-04 删了),经 Runner.accept 进来。
|
|
3
|
+
* 验正文、话题、首页按钮、小课堂、activity、照片,拼成 Runner.send 的入参;不对回 reject(状态码 + 错误名),连接上回 {t:'err'}。不起老师。
|
|
5
4
|
*/
|
|
6
5
|
import { FocusSchema, HomeViaSchema, DeviceSchema } from "../schema/index.js";
|
|
7
6
|
import { BUNDLE_ID_RE } from "../board/cards/kind.js";
|
|
@@ -30,6 +30,9 @@ export interface LiveDeps {
|
|
|
30
30
|
ringMs?: number;
|
|
31
31
|
/** 测试:要嘴说了多久一个字没出算卡住 */
|
|
32
32
|
stallMs?: number;
|
|
33
|
+
/** 测试:录像这一段多久没动静、录了多久就切(心跳时看) */
|
|
34
|
+
idleCutMs?: number;
|
|
35
|
+
maxTakeMs?: number;
|
|
33
36
|
onFinished?(why: string): void;
|
|
34
37
|
}
|
|
35
38
|
export declare class LiveCall {
|
|
@@ -110,6 +113,8 @@ export declare class LiveCall {
|
|
|
110
113
|
private chatQuiet;
|
|
111
114
|
private chatQuietUntil;
|
|
112
115
|
private offEvents;
|
|
116
|
+
/** 录像一段一段排着落盘(按顺序、一次混一段);挂了等它们都落完 */
|
|
117
|
+
private saving;
|
|
113
118
|
private ended;
|
|
114
119
|
private readonly sock;
|
|
115
120
|
private readonly deps;
|
|
@@ -155,6 +160,13 @@ export declare class LiveCall {
|
|
|
155
160
|
private mouthEnd;
|
|
156
161
|
/** 孩子按下(或不是说的那一轮开始):嘴在出就掐、还在放就清孩子端;排着的丢掉;记下孩子听到第几句 */
|
|
157
162
|
private interrupt;
|
|
163
|
+
/**
|
|
164
|
+
* 录像切段:两轮之间安静着(孩子没在说、嘴没在说也没在放、脑没在写、没在等转写),这一段没动静够久了或录得够长了,就收下落盘。
|
|
165
|
+
* 一轮不会被切开:录像按轮在一段里找按下、松手、turn 和 read
|
|
166
|
+
*/
|
|
167
|
+
private maybeCut;
|
|
168
|
+
/** 落一段录像(排在前面那几段后面):没动静的、只有连上那两条的不落 */
|
|
169
|
+
private saveTake;
|
|
158
170
|
/** 孩子的一条文本消息 */
|
|
159
171
|
onText(raw: string): void;
|
|
160
172
|
/** 一条孩子端的消息;at = 到的墙钟(现连时补的那几条照原来到的时刻记事件) */
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 没念的拍丢掉,孩子听到第几句(src/runtime/delivery/playback.ts 按声音排到哪算)进下一轮的上下文包 live:。嘴只接住、只照念,不讲新内容(2026-10-19 P1);画板开着时例外(边画边聊,见下)。
|
|
8
8
|
*
|
|
9
9
|
* 孩子端 → 这里:二进制帧 = PCM16 16k 单声道一块(按着时才推);文本帧 = JSON
|
|
10
|
-
* {t:'hold'} 按下 · {t:'release', body} 松手(body 同
|
|
10
|
+
* {t:'hold'} 按下 · {t:'release', body} 松手(body 同 send 的那条少 text:thread / newThread / device…,runner.accept 验)
|
|
11
11
|
* {t:'hush'} 停下不开新一轮(上滑取消):嘴在出就掐、孩子端清了、没念的丢掉
|
|
12
12
|
* {t:'send', body} 不是说的(打字、拍照、交卡、首页按钮;舞台开着带 focus.card):body 同那条 POST · {t:'view', w, h} 屏幕多大 · {t:'bye'}
|
|
13
13
|
* {t:'staged'} 讲稿 [[play]] 的小课堂那一段放完了(或放不了、被关):后面的讲稿接着交给嘴
|
|
@@ -31,12 +31,15 @@
|
|
|
31
31
|
* 孩子说完(像跟读,src/scenes/talk/warmup.ts)开闸下一拍,不接住、不交脑;对老师说的照收下办(打断、接住、交脑);ECHO_WAIT_MS 没开口接着走,连着两拍没开口停下交脑(app: quiet)。
|
|
32
32
|
* 每拍记进摸底(录像 {t:'echo'},只记不发),交脑时带上(live.warmup)。[[call]] 那句念完孩子端响铃,这里马上交脑一轮(app: call,不接住),
|
|
33
33
|
* 之后的接住换英文(cue.ts 的 CATCH_EN),交脑带打电话几分钟(live.phone)。孩子端连上发来的第一条带 talk(口语单)时嘴先说「好,我们先热热身!」。
|
|
34
|
-
* 录像:进出的消息、两边的声音都记(CallRecorder)
|
|
34
|
+
* 录像:进出的消息、两边的声音都记(CallRecorder),落在 conversations/<老师>/<日期>.live/<时分秒>.reel.json / .wav(时分秒 = 这一段开始的时刻)。
|
|
35
|
+
* 按段落(2026-11-06):两轮之间安静着(心跳时看),这一段 IDLE_CUT_MS 没动静、或录了 MAX_TAKE_MS,就收下这一段落盘、接着录新的一段;挂了落最后一段。
|
|
36
|
+
* 一段一轮都不会切开(录像按轮在一段里找按下、松手、turn、read);没动静的一段不落。
|
|
35
37
|
* 字幕重放:嘴照念的声音按轮另留一份,出了的都留(孩子按下时清掉、没放出来的也在),
|
|
36
38
|
* 一段出完(或被掐、断了)就落 <日期>.<job>.mouth.wav 与每句的起点 .mouth.json(src/runtime/journal/mouth-index.ts);接住那句不在里面。孩子端从 /api/kid/conversations/<老师>/mouth/<日期>/<job> 取。
|
|
37
39
|
* 事件:嘴这一道(mouth:按下、松手 / 发了、接住开始放、第几句开始放、打断)补进那一轮的 events.jsonl(Runner.noteEvent),工作台的时间线上看;
|
|
38
40
|
* 按下、松手、接住在脑起来之前,先攒着,等那一轮的老师进程起来(main:start)再按它的起点换成相对毫秒(负的)。
|
|
39
41
|
*/
|
|
42
|
+
import { existsSync } from 'node:fs';
|
|
40
43
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
41
44
|
import { join } from 'node:path';
|
|
42
45
|
import { localDate } from "../../shared/time.js";
|
|
@@ -50,7 +53,7 @@ import { wavOfPcm } from "../../shared/wav.js";
|
|
|
50
53
|
import { mouthInstructions } from "../../realtime/prompt.js";
|
|
51
54
|
import { mouthVoice } from "../../schema/index.js";
|
|
52
55
|
import { INPUT_RATE, MAX_IMAGE_B64, OUTPUT_RATE, RealtimeSession } from "../../realtime/bailian.js";
|
|
53
|
-
import { CallRecorder } from "../journal/recorder.js";
|
|
56
|
+
import { CallRecorder, renderTake } from "../journal/recorder.js";
|
|
54
57
|
import { MouthEvents, MouthTakes } from "../journal/mouth.js";
|
|
55
58
|
import { Turns } from "./turns.js";
|
|
56
59
|
/** 嘴接住的那句最多等多久再把孩子的话交给脑(接住的字一般 1 秒内齐;脑知道嘴说了什么,第一句就不再重复) */
|
|
@@ -77,6 +80,12 @@ const IDLE_SWAP_MS = 240_000;
|
|
|
77
80
|
/** 热身:[[echo]] / [[say]] 那句在孩子端放完,这么久没开口就接着下一拍(不催);连着这么多拍没开口就停下交脑 */
|
|
78
81
|
const ECHO_WAIT_MS = 8000;
|
|
79
82
|
const QUIET_BEATS = 2;
|
|
83
|
+
/**
|
|
84
|
+
* 录像切段:这一段这么久没动静、或录了这么久(两轮之间安静着才切),就收下落盘、接着录新的一段。
|
|
85
|
+
* 页面开着不关能挂几个钟头(2026-10-25 一段 2 小时、361 MB 的 wav,几乎没说话):不切的话声音全攒在内存里,挂断时一起混
|
|
86
|
+
*/
|
|
87
|
+
const IDLE_CUT_MS = 5 * 60_000;
|
|
88
|
+
const MAX_TAKE_MS = 30 * 60_000;
|
|
80
89
|
/** 打电话:[[call]] 那句放完先响这么久的铃,脑的英文开场来得再早也等它(孩子听得到在打电话) */
|
|
81
90
|
const RING_MS = 3000;
|
|
82
91
|
/**
|
|
@@ -170,6 +179,8 @@ export class LiveCall {
|
|
|
170
179
|
chatQuiet = 0;
|
|
171
180
|
chatQuietUntil = 0;
|
|
172
181
|
offEvents = null;
|
|
182
|
+
/** 录像一段一段排着落盘(按顺序、一次混一段);挂了等它们都落完 */
|
|
183
|
+
saving = Promise.resolve();
|
|
173
184
|
ended = false;
|
|
174
185
|
sock;
|
|
175
186
|
deps;
|
|
@@ -526,6 +537,7 @@ export class LiveCall {
|
|
|
526
537
|
keepAlive() {
|
|
527
538
|
const s = this.session;
|
|
528
539
|
this.maybeSwap();
|
|
540
|
+
this.maybeCut();
|
|
529
541
|
if (!s?.isReady || this.holding || this.vadOn || s.responding || Date.now() - this.lastActive > (this.deps.keepAliveMs ?? KEEP_ALIVE_MS))
|
|
530
542
|
return;
|
|
531
543
|
// 一直听、声音一直在推:本来就有输入,不用跳
|
|
@@ -563,6 +575,32 @@ export class LiveCall {
|
|
|
563
575
|
this.send({ t: 'flush' });
|
|
564
576
|
}
|
|
565
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* 录像切段:两轮之间安静着(孩子没在说、嘴没在说也没在放、脑没在写、没在等转写),这一段没动静够久了或录得够长了,就收下落盘。
|
|
580
|
+
* 一轮不会被切开:录像按轮在一段里找按下、松手、turn 和 read
|
|
581
|
+
*/
|
|
582
|
+
maybeCut() {
|
|
583
|
+
if (this.ended || !this.quiet() || this.playing() || this.pendingVoice || this.draft || this.echoing || this.chatHeard !== null)
|
|
584
|
+
return;
|
|
585
|
+
const idle = this.rec.quietMs() >= (this.deps.idleCutMs ?? IDLE_CUT_MS);
|
|
586
|
+
const long = this.rec.stirred && this.rec.takeMs() >= (this.deps.maxTakeMs ?? MAX_TAKE_MS);
|
|
587
|
+
if (idle || long)
|
|
588
|
+
this.saveTake(this.rec.cut());
|
|
589
|
+
}
|
|
590
|
+
/** 落一段录像(排在前面那几段后面):没动静的、只有连上那两条的不落 */
|
|
591
|
+
saveTake(raw) {
|
|
592
|
+
if (!raw.stirred || raw.msgs.length <= 2)
|
|
593
|
+
return;
|
|
594
|
+
const { ctx, tutor } = this.deps;
|
|
595
|
+
this.saving = this.saving.then(async () => {
|
|
596
|
+
const take = await renderTake(raw);
|
|
597
|
+
const dir = join(ctx.runner.ws.dirs.conversations, tutor, `${localDate(raw.startedAt)}.live`);
|
|
598
|
+
await mkdir(dir, { recursive: true });
|
|
599
|
+
const name = takeName(dir, raw.startedAt);
|
|
600
|
+
await writeFile(join(dir, `${name}.wav`), wavOfPcm(take.callPcm, MIX_RATE));
|
|
601
|
+
await writeFile(join(dir, `${name}.reel.json`), JSON.stringify(take.reel));
|
|
602
|
+
}).catch((err) => { console.error('live: 录像落盘失败', err); });
|
|
603
|
+
}
|
|
566
604
|
/** 孩子的一条文本消息 */
|
|
567
605
|
onText(raw) {
|
|
568
606
|
if (this.ended)
|
|
@@ -1155,21 +1193,8 @@ export class LiveCall {
|
|
|
1155
1193
|
if (this.said?.kind === 'read')
|
|
1156
1194
|
this.mouthEnd(this.said, false);
|
|
1157
1195
|
await this.takes.flush();
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
if (take.reel.msgs.length > 2) {
|
|
1161
|
-
const { ctx, tutor } = this.deps;
|
|
1162
|
-
const at = this.rec.startedAt;
|
|
1163
|
-
const dir = join(ctx.runner.ws.dirs.conversations, tutor, `${localDate(at)}.live`);
|
|
1164
|
-
const name = [at.getHours(), at.getMinutes(), at.getSeconds()].map((n) => String(n).padStart(2, '0')).join('');
|
|
1165
|
-
await mkdir(dir, { recursive: true });
|
|
1166
|
-
await writeFile(join(dir, `${name}.wav`), wavOfPcm(take.callPcm, MIX_RATE));
|
|
1167
|
-
await writeFile(join(dir, `${name}.reel.json`), JSON.stringify(take.reel));
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
catch (err) {
|
|
1171
|
-
console.error('live: 录像落盘失败', err);
|
|
1172
|
-
}
|
|
1196
|
+
this.saveTake(this.rec.finish());
|
|
1197
|
+
await this.saving;
|
|
1173
1198
|
this.sock.close(1000, why);
|
|
1174
1199
|
this.deps.onFinished?.(why);
|
|
1175
1200
|
}
|
|
@@ -1178,6 +1203,14 @@ export class LiveCall {
|
|
|
1178
1203
|
void this.end('kid-gone'); }
|
|
1179
1204
|
}
|
|
1180
1205
|
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
1206
|
+
/** 一段录像的文件名:开始的时分秒;这一秒已经有一段了(同一位老师两次通话、切段撞在同一秒)往后挪一秒,不盖掉 */
|
|
1207
|
+
const takeName = (dir, at) => {
|
|
1208
|
+
const hms = (d) => [d.getHours(), d.getMinutes(), d.getSeconds()].map((n) => String(n).padStart(2, '0')).join('');
|
|
1209
|
+
let name = hms(at);
|
|
1210
|
+
for (let k = 1; k < 60 && existsSync(join(dir, `${name}.reel.json`)); k++)
|
|
1211
|
+
name = hms(new Date(at.getTime() + k * 1000));
|
|
1212
|
+
return name;
|
|
1213
|
+
};
|
|
1181
1214
|
/** 孩子端发来的图(data:image/jpeg;base64,…)→ 字节;不是 jpeg、太大(百炼一帧上限)回 null */
|
|
1182
1215
|
const jpegOf = (v) => {
|
|
1183
1216
|
const m = typeof v === 'string' ? /^data:image\/jpeg;base64,([A-Za-z0-9+/=]+)$/.exec(v) : null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 实时那一轮交给脑(从 call.ts 拆出来,2026-10-29,行为不变;以后是 runtime/turn):
|
|
3
|
-
* 孩子端发来的那条验过、拼好(runner.accept
|
|
3
|
+
* 孩子端发来的那条验过、拼好(runner.accept;孩子端只走这条路,HTTP 的发 2026-11-04 删了),带上这一轮实时的那些(live:)交给 runner.send;
|
|
4
4
|
* 这位老师别处正在写(BusyError)就排着,它写完把排着的几句合并成一条再发;脑在写时孩子又说的(submit 时 running)也排着,这一轮写完一起发。
|
|
5
5
|
* 一轮写完,收尾时把流式没发出来的讲稿从正式的一节补上(整块出的运行时、最后几句)。
|
|
6
6
|
*/
|