disaster-broadcast-studio 0.1.1 → 0.2.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.
Files changed (59) hide show
  1. package/README.md +10 -5
  2. package/assets/README.md +1 -1
  3. package/assets/audio/README.md +16 -4
  4. package/assets/audio/alert-critical.mjs +4 -0
  5. package/assets/audio/alert-critical.wav +0 -0
  6. package/assets/audio/alert-critical.wav.json +15 -0
  7. package/assets/audio/alert-urgent.mjs +4 -0
  8. package/assets/audio/alert-urgent.wav +0 -0
  9. package/assets/audio/alert-urgent.wav.json +15 -0
  10. package/assets/audio/bulletin.mjs +9 -0
  11. package/assets/audio/bulletin.wav +0 -0
  12. package/assets/audio/bulletin.wav.json +15 -0
  13. package/assets/audio/news.mjs +4 -0
  14. package/assets/audio/news.wav +0 -0
  15. package/assets/audio/news.wav.json +15 -0
  16. package/assets/audio/notification.mjs +24 -9
  17. package/assets/audio/notification.wav +0 -0
  18. package/assets/audio/notification.wav.json +15 -0
  19. package/assets/audio/status.mjs +5 -0
  20. package/assets/audio/status.wav +0 -0
  21. package/assets/audio/status.wav.json +15 -0
  22. package/assets/audio/tones.mjs +17 -0
  23. package/assets/workspace/README.md +3 -0
  24. package/dist/cli.js +19 -1
  25. package/dist/credits.js +2 -2
  26. package/dist/load.js +5 -3
  27. package/dist/notifications.d.ts +40 -3
  28. package/dist/notifications.js +34 -6
  29. package/dist/preview/browser.d.ts +1 -0
  30. package/dist/preview/browser.js +87 -0
  31. package/dist/preview/http.d.ts +3 -0
  32. package/dist/preview/http.js +78 -0
  33. package/dist/preview/page.d.ts +1 -0
  34. package/dist/preview/page.js +30 -0
  35. package/dist/preview/project.d.ts +22 -0
  36. package/dist/preview/project.js +81 -0
  37. package/dist/preview/server.d.ts +6 -0
  38. package/dist/preview/server.js +57 -0
  39. package/dist/render.js +4 -40
  40. package/dist/scenario.d.ts +15 -0
  41. package/dist/scenario.js +19 -15
  42. package/dist/timeline.d.ts +6 -5
  43. package/dist/timeline.js +11 -8
  44. package/dist/video/Alerts.js +63 -47
  45. package/dist/video/Broadcast.js +14 -38
  46. package/dist/video/Bulletin.d.ts +2 -1
  47. package/dist/video/Bulletin.js +8 -4
  48. package/dist/video/Information.d.ts +16 -0
  49. package/dist/video/Information.js +104 -0
  50. package/dist/video/Map.js +26 -10
  51. package/dist/video/prepare.d.ts +4 -0
  52. package/dist/video/prepare.js +47 -0
  53. package/dist/video/transitions.d.ts +562 -0
  54. package/dist/video/transitions.js +31 -0
  55. package/dist/video/useBundledFonts.js +15 -6
  56. package/package.json +2 -1
  57. package/skills/scenario-writing/SKILL.md +2 -2
  58. package/skills/scenario-writing/references/audio.md +1 -1
  59. package/skills/scenario-writing/references/format.md +16 -7
@@ -0,0 +1,562 @@
1
+ import { type CompiledEvent } from '../scenario.js';
2
+ export declare const changeFrames: number;
3
+ export declare function changeProgress(frame: number, start: number, frames?: number): number;
4
+ export declare function transitionAt<T extends CompiledEvent['type']>(events: CompiledEvent[], type: T, frame: number): {
5
+ current: Extract<{
6
+ type: "alert";
7
+ sender: string;
8
+ text: string;
9
+ level: "안전안내" | "긴급재난" | "위급재난";
10
+ regions: string[];
11
+ mapHighlight: false | {
12
+ duration: number;
13
+ color: string;
14
+ };
15
+ sound: false | "default" | {
16
+ asset: string;
17
+ duration: number;
18
+ volume: number;
19
+ };
20
+ pageSeconds: number;
21
+ id: string;
22
+ cut: string;
23
+ at: number;
24
+ corrects?: string | undefined;
25
+ occurredAt?: string | undefined;
26
+ publishedAt?: string | undefined;
27
+ } & {
28
+ start: number;
29
+ end: number;
30
+ order: number;
31
+ }, {
32
+ type: T;
33
+ }> | Extract<{
34
+ type: "news";
35
+ sound: false | "default" | {
36
+ asset: string;
37
+ duration: number;
38
+ volume: number;
39
+ };
40
+ source: string;
41
+ headline: string;
42
+ duration: number;
43
+ carry: boolean;
44
+ id: string;
45
+ cut: string;
46
+ at: number;
47
+ publishedAt?: string | undefined;
48
+ } & {
49
+ start: number;
50
+ end: number;
51
+ order: number;
52
+ }, {
53
+ type: T;
54
+ }> | Extract<{
55
+ type: "status";
56
+ sound: false | "default" | {
57
+ asset: string;
58
+ duration: number;
59
+ volume: number;
60
+ };
61
+ items: {
62
+ text: string;
63
+ color: string;
64
+ }[];
65
+ duration: number;
66
+ carry: boolean;
67
+ id: string;
68
+ cut: string;
69
+ at: number;
70
+ } & {
71
+ start: number;
72
+ end: number;
73
+ order: number;
74
+ }, {
75
+ type: T;
76
+ }> | Extract<{
77
+ type: "bulletin";
78
+ sound: false | "default" | {
79
+ asset: string;
80
+ duration: number;
81
+ volume: number;
82
+ };
83
+ source: string;
84
+ heading: string;
85
+ text: string;
86
+ color: string;
87
+ pageSeconds: number;
88
+ duration: number;
89
+ carry: boolean;
90
+ id: string;
91
+ cut: string;
92
+ at: number;
93
+ } & {
94
+ start: number;
95
+ end: number;
96
+ order: number;
97
+ }, {
98
+ type: T;
99
+ }> | Extract<{
100
+ type: "zone";
101
+ points: [number, number][];
102
+ color: string;
103
+ opacity: number;
104
+ duration: number;
105
+ carry: boolean;
106
+ id: string;
107
+ cut: string;
108
+ at: number;
109
+ } & {
110
+ start: number;
111
+ end: number;
112
+ order: number;
113
+ }, {
114
+ type: T;
115
+ }> | Extract<{
116
+ type: "line";
117
+ points: [number, number][];
118
+ color: string;
119
+ width: number;
120
+ drawSeconds: number;
121
+ dashed: boolean;
122
+ duration: number;
123
+ carry: boolean;
124
+ id: string;
125
+ cut: string;
126
+ at: number;
127
+ } & {
128
+ start: number;
129
+ end: number;
130
+ order: number;
131
+ }, {
132
+ type: T;
133
+ }> | Extract<{
134
+ type: "region";
135
+ regions: string[];
136
+ color: string;
137
+ fillOpacity: number;
138
+ strokeOpacity: number;
139
+ strokeWidth: number;
140
+ fadeIn: number;
141
+ fadeOut: number;
142
+ pulse: boolean;
143
+ duration: number;
144
+ carry: boolean;
145
+ id: string;
146
+ cut: string;
147
+ at: number;
148
+ strokeColor?: string | undefined;
149
+ label?: string | undefined;
150
+ } & {
151
+ start: number;
152
+ end: number;
153
+ order: number;
154
+ }, {
155
+ type: T;
156
+ }> | Extract<{
157
+ type: "legend";
158
+ items: {
159
+ text: string;
160
+ color: string;
161
+ symbol: "line" | "fill" | "outline";
162
+ }[];
163
+ duration: number;
164
+ carry: boolean;
165
+ id: string;
166
+ cut: string;
167
+ at: number;
168
+ } & {
169
+ start: number;
170
+ end: number;
171
+ order: number;
172
+ }, {
173
+ type: T;
174
+ }> | Extract<{
175
+ type: "marker";
176
+ position: [number, number];
177
+ color: string;
178
+ effect: "point" | "ring";
179
+ duration: number;
180
+ carry: boolean;
181
+ id: string;
182
+ cut: string;
183
+ at: number;
184
+ label?: string | undefined;
185
+ } & {
186
+ start: number;
187
+ end: number;
188
+ order: number;
189
+ }, {
190
+ type: T;
191
+ }> | Extract<{
192
+ type: "camera";
193
+ to: {
194
+ center: [number, number];
195
+ zoom: number;
196
+ bearing: number;
197
+ pitch: number;
198
+ };
199
+ ease: "linear" | "smooth";
200
+ duration: number;
201
+ carry: boolean;
202
+ id: string;
203
+ cut: string;
204
+ at: number;
205
+ } & {
206
+ start: number;
207
+ end: number;
208
+ order: number;
209
+ }, {
210
+ type: T;
211
+ }> | Extract<{
212
+ type: "inset";
213
+ camera: {
214
+ center: [number, number];
215
+ zoom: number;
216
+ bearing: number;
217
+ pitch: number;
218
+ };
219
+ title: string;
220
+ duration: number;
221
+ carry: boolean;
222
+ id: string;
223
+ cut: string;
224
+ at: number;
225
+ } & {
226
+ start: number;
227
+ end: number;
228
+ order: number;
229
+ }, {
230
+ type: T;
231
+ }> | Extract<{
232
+ type: "audio";
233
+ asset: string;
234
+ offset: number;
235
+ volume: number;
236
+ fadeIn: number;
237
+ fadeOut: number;
238
+ duration: number;
239
+ carry: boolean;
240
+ id: string;
241
+ cut: string;
242
+ at: number;
243
+ } & {
244
+ start: number;
245
+ end: number;
246
+ order: number;
247
+ }, {
248
+ type: T;
249
+ }> | Extract<{
250
+ type: "silence";
251
+ duration: number;
252
+ carry: boolean;
253
+ id: string;
254
+ cut: string;
255
+ at: number;
256
+ } & {
257
+ start: number;
258
+ end: number;
259
+ order: number;
260
+ }, {
261
+ type: T;
262
+ }> | Extract<{
263
+ type: "screen";
264
+ effect: "blackout" | "flash" | "interference" | "noise" | "scanlines" | "chromatic" | "dropout" | "shutdown";
265
+ intensity: number;
266
+ fadeIn: number;
267
+ fadeOut: number;
268
+ duration: number;
269
+ carry: boolean;
270
+ id: string;
271
+ cut: string;
272
+ at: number;
273
+ } & {
274
+ start: number;
275
+ end: number;
276
+ order: number;
277
+ }, {
278
+ type: T;
279
+ }> | undefined;
280
+ previous: Extract<{
281
+ type: "alert";
282
+ sender: string;
283
+ text: string;
284
+ level: "안전안내" | "긴급재난" | "위급재난";
285
+ regions: string[];
286
+ mapHighlight: false | {
287
+ duration: number;
288
+ color: string;
289
+ };
290
+ sound: false | "default" | {
291
+ asset: string;
292
+ duration: number;
293
+ volume: number;
294
+ };
295
+ pageSeconds: number;
296
+ id: string;
297
+ cut: string;
298
+ at: number;
299
+ corrects?: string | undefined;
300
+ occurredAt?: string | undefined;
301
+ publishedAt?: string | undefined;
302
+ } & {
303
+ start: number;
304
+ end: number;
305
+ order: number;
306
+ }, {
307
+ type: T;
308
+ }> | Extract<{
309
+ type: "news";
310
+ sound: false | "default" | {
311
+ asset: string;
312
+ duration: number;
313
+ volume: number;
314
+ };
315
+ source: string;
316
+ headline: string;
317
+ duration: number;
318
+ carry: boolean;
319
+ id: string;
320
+ cut: string;
321
+ at: number;
322
+ publishedAt?: string | undefined;
323
+ } & {
324
+ start: number;
325
+ end: number;
326
+ order: number;
327
+ }, {
328
+ type: T;
329
+ }> | Extract<{
330
+ type: "status";
331
+ sound: false | "default" | {
332
+ asset: string;
333
+ duration: number;
334
+ volume: number;
335
+ };
336
+ items: {
337
+ text: string;
338
+ color: string;
339
+ }[];
340
+ duration: number;
341
+ carry: boolean;
342
+ id: string;
343
+ cut: string;
344
+ at: number;
345
+ } & {
346
+ start: number;
347
+ end: number;
348
+ order: number;
349
+ }, {
350
+ type: T;
351
+ }> | Extract<{
352
+ type: "bulletin";
353
+ sound: false | "default" | {
354
+ asset: string;
355
+ duration: number;
356
+ volume: number;
357
+ };
358
+ source: string;
359
+ heading: string;
360
+ text: string;
361
+ color: string;
362
+ pageSeconds: number;
363
+ duration: number;
364
+ carry: boolean;
365
+ id: string;
366
+ cut: string;
367
+ at: number;
368
+ } & {
369
+ start: number;
370
+ end: number;
371
+ order: number;
372
+ }, {
373
+ type: T;
374
+ }> | Extract<{
375
+ type: "zone";
376
+ points: [number, number][];
377
+ color: string;
378
+ opacity: number;
379
+ duration: number;
380
+ carry: boolean;
381
+ id: string;
382
+ cut: string;
383
+ at: number;
384
+ } & {
385
+ start: number;
386
+ end: number;
387
+ order: number;
388
+ }, {
389
+ type: T;
390
+ }> | Extract<{
391
+ type: "line";
392
+ points: [number, number][];
393
+ color: string;
394
+ width: number;
395
+ drawSeconds: number;
396
+ dashed: boolean;
397
+ duration: number;
398
+ carry: boolean;
399
+ id: string;
400
+ cut: string;
401
+ at: number;
402
+ } & {
403
+ start: number;
404
+ end: number;
405
+ order: number;
406
+ }, {
407
+ type: T;
408
+ }> | Extract<{
409
+ type: "region";
410
+ regions: string[];
411
+ color: string;
412
+ fillOpacity: number;
413
+ strokeOpacity: number;
414
+ strokeWidth: number;
415
+ fadeIn: number;
416
+ fadeOut: number;
417
+ pulse: boolean;
418
+ duration: number;
419
+ carry: boolean;
420
+ id: string;
421
+ cut: string;
422
+ at: number;
423
+ strokeColor?: string | undefined;
424
+ label?: string | undefined;
425
+ } & {
426
+ start: number;
427
+ end: number;
428
+ order: number;
429
+ }, {
430
+ type: T;
431
+ }> | Extract<{
432
+ type: "legend";
433
+ items: {
434
+ text: string;
435
+ color: string;
436
+ symbol: "line" | "fill" | "outline";
437
+ }[];
438
+ duration: number;
439
+ carry: boolean;
440
+ id: string;
441
+ cut: string;
442
+ at: number;
443
+ } & {
444
+ start: number;
445
+ end: number;
446
+ order: number;
447
+ }, {
448
+ type: T;
449
+ }> | Extract<{
450
+ type: "marker";
451
+ position: [number, number];
452
+ color: string;
453
+ effect: "point" | "ring";
454
+ duration: number;
455
+ carry: boolean;
456
+ id: string;
457
+ cut: string;
458
+ at: number;
459
+ label?: string | undefined;
460
+ } & {
461
+ start: number;
462
+ end: number;
463
+ order: number;
464
+ }, {
465
+ type: T;
466
+ }> | Extract<{
467
+ type: "camera";
468
+ to: {
469
+ center: [number, number];
470
+ zoom: number;
471
+ bearing: number;
472
+ pitch: number;
473
+ };
474
+ ease: "linear" | "smooth";
475
+ duration: number;
476
+ carry: boolean;
477
+ id: string;
478
+ cut: string;
479
+ at: number;
480
+ } & {
481
+ start: number;
482
+ end: number;
483
+ order: number;
484
+ }, {
485
+ type: T;
486
+ }> | Extract<{
487
+ type: "inset";
488
+ camera: {
489
+ center: [number, number];
490
+ zoom: number;
491
+ bearing: number;
492
+ pitch: number;
493
+ };
494
+ title: string;
495
+ duration: number;
496
+ carry: boolean;
497
+ id: string;
498
+ cut: string;
499
+ at: number;
500
+ } & {
501
+ start: number;
502
+ end: number;
503
+ order: number;
504
+ }, {
505
+ type: T;
506
+ }> | Extract<{
507
+ type: "audio";
508
+ asset: string;
509
+ offset: number;
510
+ volume: number;
511
+ fadeIn: number;
512
+ fadeOut: number;
513
+ duration: number;
514
+ carry: boolean;
515
+ id: string;
516
+ cut: string;
517
+ at: number;
518
+ } & {
519
+ start: number;
520
+ end: number;
521
+ order: number;
522
+ }, {
523
+ type: T;
524
+ }> | Extract<{
525
+ type: "silence";
526
+ duration: number;
527
+ carry: boolean;
528
+ id: string;
529
+ cut: string;
530
+ at: number;
531
+ } & {
532
+ start: number;
533
+ end: number;
534
+ order: number;
535
+ }, {
536
+ type: T;
537
+ }> | Extract<{
538
+ type: "screen";
539
+ effect: "blackout" | "flash" | "interference" | "noise" | "scanlines" | "chromatic" | "dropout" | "shutdown";
540
+ intensity: number;
541
+ fadeIn: number;
542
+ fadeOut: number;
543
+ duration: number;
544
+ carry: boolean;
545
+ id: string;
546
+ cut: string;
547
+ at: number;
548
+ } & {
549
+ start: number;
550
+ end: number;
551
+ order: number;
552
+ }, {
553
+ type: T;
554
+ }> | undefined;
555
+ progress: number;
556
+ start: number;
557
+ };
558
+ export declare function pageAt(frame: number, start: number, seconds: number, count: number): {
559
+ page: number;
560
+ previous: number | undefined;
561
+ progress: number;
562
+ };
@@ -0,0 +1,31 @@
1
+ import { FPS } from '../scenario.js';
2
+ // Frame-derived motion: seeking and parallel renders never depend on DOM history.
3
+ export const changeFrames = Math.round(0.45 * FPS);
4
+ export function changeProgress(frame, start, frames = changeFrames) {
5
+ const p = Math.max(0, Math.min(1, (frame - start) / frames));
6
+ return 1 - (1 - p) ** 3;
7
+ }
8
+ export function transitionAt(events, type, frame) {
9
+ const candidates = events.filter((e) => e.type === type && e.start <= frame);
10
+ const current = candidates.find((e) => frame < e.end);
11
+ const previous = current
12
+ ? candidates.find((e) => e !== current && e.end === current.start)
13
+ : candidates.reduce((last, e) => (!last || e.end > last.end ? e : last), undefined);
14
+ const start = current?.start ?? previous?.end ?? frame;
15
+ const progress = changeProgress(frame, start);
16
+ return {
17
+ current,
18
+ previous: progress < 1 ? previous : undefined,
19
+ progress,
20
+ start,
21
+ };
22
+ }
23
+ export function pageAt(frame, start, seconds, count) {
24
+ const frames = Math.round(seconds * FPS);
25
+ const turn = Math.floor(Math.max(0, frame - start) / frames);
26
+ return {
27
+ page: turn % count,
28
+ previous: turn > 0 && count > 1 ? (turn - 1) % count : undefined,
29
+ progress: turn > 0 && count > 1 ? changeProgress(frame, start + turn * frames) : 1,
30
+ };
31
+ }
@@ -1,10 +1,13 @@
1
1
  import { useEffect, useState } from 'react';
2
- import { staticFile, useDelayRender, cancelRender } from 'remotion';
2
+ import { staticFile, useBufferState, useDelayRender } from 'remotion';
3
3
  import { fonts } from './fonts.js';
4
4
  export function useBundledFonts(text) {
5
- const { delayRender, continueRender } = useDelayRender();
5
+ const { delayRender, continueRender, cancelRender } = useDelayRender();
6
+ const buffer = useBufferState();
6
7
  const [fontHandle] = useState(() => delayRender('Loading bundled Korean font'));
7
8
  useEffect(() => {
9
+ const playback = buffer.delayPlayback();
10
+ let cancelled = false;
8
11
  const links = fonts.flatMap((font) => font.weights.map((weight) => {
9
12
  const link = document.createElement('link');
10
13
  link.rel = 'stylesheet';
@@ -20,8 +23,14 @@ export function useBundledFonts(text) {
20
23
  ? '0123456789:.- /+'
21
24
  : text + ' 재난문자 수신 이력 발생 발표 이전 정정 안전안내 긴급재난 위급재난')))))
22
25
  .then(() => document.fonts.ready)
23
- .then(() => continueRender(fontHandle))
24
- .catch(cancelRender);
25
- return () => links.forEach((link) => link.remove());
26
- }, [text, fontHandle, delayRender, continueRender]);
26
+ .then(() => { if (!cancelled) {
27
+ continueRender(fontHandle);
28
+ playback.unblock();
29
+ } })
30
+ .catch(error => { if (!cancelled) {
31
+ playback.unblock();
32
+ cancelRender(error);
33
+ } });
34
+ return () => { cancelled = true; playback.unblock(); continueRender(fontHandle); links.forEach((link) => link.remove()); };
35
+ }, [text, fontHandle, buffer, delayRender, continueRender, cancelRender]);
27
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "disaster-broadcast-studio",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Render text-authored Korean disaster broadcast fiction into deterministic videos.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,7 @@
27
27
  "@fontsource/ibm-plex-mono": "5.3.0",
28
28
  "@fontsource/ibm-plex-sans-kr": "5.3.0",
29
29
  "@remotion/bundler": "4.0.543",
30
+ "@remotion/player": "4.0.543",
30
31
  "@remotion/renderer": "4.0.543",
31
32
  "commander": "^14.0.0",
32
33
  "maplibre-gl": "6.18.0",
@@ -31,7 +31,7 @@ description: 재난방송실을 이용하는 창작 AI가 한국형 재난방송
31
31
  1. 사용자가 정한 소재·길이·분위기·매체를 바탕으로 사건의 흐름을 설계한다. 미정인 창작 선택은 작품에 맞춰 결정하되 이미 합의한 조건을 바꾸지 않는다.
32
32
  2. 사건 내용과 관객이 정보를 보게 되는 시점을 구분한다. 사건 발생·발령·보도·수신 시각이 다르면 그 차이를 명시한다.
33
33
  3. 문자의 대상·행동·발신자와 뉴스의 발표 주체·확인 범위를 구별한다. 발신기관과 발송 대상을 혼동하지 않으며, 모든 문자에서 본문·지역 코드·지도 범위를 맞춘다. 도 전역이면 소속 시군 전체를 조회한다. 수신 시 채우기가 부드럽게 나타났다 사라지는 지도 강조는 기본 기능이며 필요하면 끈다.
34
- 4. 공통 문자 수신음은 엔진 기본 기능을 쓰며 작품별로 중복 제작하지 않는다. [Tone.js 음원 제작](references/audio.md)의 실행 가능한 예제를 활용해 필요한 소리를 만든다. 기존 로컬 음원도 사용할 수 있으며 출처·사용권을 기록한다.
34
+ 4. 상태·방송 안내·등급별 문자 알림음은 엔진 기본 기능을 쓰며 작품별로 중복 제작하지 않는다. 뉴스는 기본 무음으로 두고 필요한 경우에만 소리를 명시한다. 이벤트의 `sound`로 끄거나 교체한다. [Tone.js 음원 제작](references/audio.md)의 실행 가능한 예제를 활용해 필요한 소리를 만든다. 기존 로컬 음원도 사용할 수 있으며 출처·사용권을 기록한다.
35
35
  5. 내용과 함께 등장·퇴장·지도 이동·강조·음향·침묵·전환의 시점을 작성한다. 컷 내부 시간은 현실과 1:1로 흐르게 하고, 시간 생략은 명시적 컷 경계의 `clockTransition`으로 표현한다. 기존 상단 시계의 강조·숫자 이동·도착 시각 유지가 읽힌 뒤 새 정보를 배치한다.
36
36
  6. 절제된 방송 화면을 기본으로 삼되 모든 작품에 같은 긴장 장치나 반전을 강제하지 않는다. 행정 표현의 현실감과 읽을 시간을 확보한다.
37
37
 
@@ -39,7 +39,7 @@ description: 재난방송실을 이용하는 창작 AI가 한국형 재난방송
39
39
 
40
40
  [작품 파일과 CLI](references/format.md)에 따라 외부 작품 파일을 작성한다. 정확한 필드 계약은 설치된 CLI의 `dbs schema`, 명령은 `dbs --help`로 확인한다.
41
41
 
42
- `dbs validate` → `dbs frame`과 구간 `dbs render` → 문안 가독성·지도 범위·음향·시간 수정 → 전체 `dbs render` 순으로 진행한다. 중요 장면의 앞뒤 프레임과 컷 경계를 확인하며 매번 전체 영상을 다시 출력하지 않는다. 작품 데이터에서 고칠 수 있는 오류는 그곳에서 고치고, 엔진 기능이 부족하면 필요한 기능을 구체적으로 분리한다. 창작 중 엔진 코드를 임의로 바꾸지 않는다.
42
+ `dbs validate` → `dbs preview`로 화면·음향 재생과 탐색 → 문안 가독성·지도 범위·음향·시간 수정 → 필요한 `dbs frame`·구간 `dbs render` 검증 → 전체 `dbs render` 순으로 진행한다. 웹 미리보기는 저장한 변경을 현재 위치에서 일시정지해 반영한다. 중요 장면의 앞뒤 프레임과 컷 경계를 확인하며 매번 전체 영상을 다시 출력하지 않는다. 작품 데이터에서 고칠 수 있는 오류는 그곳에서 고치고, 엔진 기능이 부족하면 필요한 기능을 구체적으로 분리한다. 창작 중 엔진 코드를 임의로 바꾸지 않는다.
43
43
 
44
44
  ## 결과물 확인
45
45
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 실행 가능한 시작점
6
6
 
7
- 문자 수신음은 엔진 공통 기능이다. 생략 시 창작 알림음이 자동 재생되며 작품마다 생성하지 않는다. `alert.sound: false`로 끄거나 `{asset, duration, volume}`으로 다른 음원을 연결한다. 기존 작품의 수신음 `audio` 이벤트와 중복하지 않는다.
7
+ 상태 변경·방송 안내·등급별 문자 수신음은 엔진 기본 기능이며 작품마다 생성하지 않는다. 뉴스는 기본 무음이며 `sound: default` 또는 사용자 음원을 명시한 경우에만 소리를 재생한다. 상태 변경음은 화면 교체 시작에 같은 높이로 두 번 울리는 0.3초의 짧은 전자음이다. 안전안내는 0.55초의 휴대폰 진동형 수신음, 긴급재난은 2.4초의 반복 경보, 위급재난은 4.2초의 더 강한 반복·지속 경보다. 긴급·위급은 EAS풍의 불협화음으로 합성했다. `alert`, `status`, `news`, `bulletin`에서 `sound: false`로 끄거나 `{asset, duration, volume}`으로 다른 음원을 연결한다. 기본음과 별도 `audio` 이벤트를 중복하지 않는다. `silence`는 기본음도 음소거한다.
8
8
 
9
9
  아래 합성은 작품 고유 음향이 필요한 경우에 사용한다. 스킬의 `examples/receipt.mjs`를 `audio-code/receipt.mjs`로 직접 복사해 실행할 수 있다. CC0-1.0의 합성 예제이며 실제 정부 경보음이 아니다.
10
10