| name | script-writing |
| description | Write TTS-ready transcripts with [slide N] cues, [pause:ms] markers, and optional SSML. Convert raw code/URLs/long numbers to speakable forms. Use when turning slides+beats into a narrator script. Triggered by the script-writer agent. |
Script Writing โ Speakable Transcript
transcript๋ ๊ท๋ก ๋ฃ๋ ์ฝํ
์ธ ๋ค. note๋ฅผ ๊ทธ๋๋ก ๋ญ๋
ํ๋ฉด ๋ถ์์ฐ์ค๋ฝ๋ค. ๋ฌธ์ฅ ๊ธธ์ด, ๋ฆฌ๋ฌ, ํธํก, ๊ฐ์กฐ๋ฅผ ์์ฑ ๊ธฐ์ค์ผ๋ก ์ฌ์ค๊ณ.
๊ตฌ์กฐ (plain text)
[slide 1]
์ฒซ ๋ฌธ์ฅ. ๋ฆฌ๋ฌ์ ์ํ ์งง์ ๋ฌธ์ฅ.
[pause:400]
๋ ๋ฒ์งธ ๋ฌธ์ฅ์ ์กฐ๊ธ ๋ ๊ธธ์ด๋ ๊ด์ฐฎ์ง๋ง 25 ๋จ์ด๋ฅผ ๋์ง ์๋๋ก ํฉ๋๋ค.
[pause:600]
ํต์ฌ ๊ฐ์กฐ ์ ์๋ ์กฐ๊ธ ๋ ์ฌ์ด์ค๋๋ค.
[slide 2]
์ ์ฌ๋ผ์ด๋๋ก ๋์ด๊ฐ๋๋ค.
...
Line layout (subtitle-sync ํ์)
ํ ์ฌ๋ผ์ด๋์ ๋ด๋ ์ด์
์ ์ ๋ ํ ์ค์ ๋ชฐ์๋ฃ์ง ๋ง ๊ฒ. generate-player.py์ parse_transcript_by_slide๋ newline์ผ๋ก ๋ถํ ํด ๊ฐ ์ค์ ์๋ง ์ถ์ ๋จ์๋ก ์ฌ์ฉํ๊ณ , char_proportional_line_times๊ฐ ๊ฐ ์ค์ ๊ธ์ ์ ๋น์จ๋ก ์ฌ๋ผ์ด๋ MP3 duration์ ์ชผ๊ฐ {start,end}๋ฅผ ๋ถ์ฌํ๋ค. ํ ์ค์ง๋ฆฌ ์ฌ๋ผ์ด๋๋ ์๋ง ํ ๋ฉ์ด๋ฆฌ + ํ์ด๋ผ์ดํธ ๋ถ๊ฐ.
๊ท์น:
- ๋ฐํ ๋จ์ ํ ๊ฐ = ํ ์ค. ๋ฌธ์ฅ ๋๋
[pause:โฆ] ์ฌ์ด์ ํ ํธํก ๋จ์.
[pause:NNN] ๋ง์ปค๋ ์๊ธฐ๋ง์ ์ค์ ๋๋ค (์ธ๋ผ์ธ ๊ธ์ง). ํ๋ ์ด์ด๊ฐ pause ์ค์ strip โ ๋น ์ค๋ก ํํฐ๋ง.
[slide N] ์ญ์ ์๊ธฐ๋ง์ ์ค.
- ๋น ์ค์ ์ฌ๋ผ์ด๋ ์ฌ์ด ๊ตฌ๋ถ์ฉ์ผ๋ก๋ง ํ์ฉ.
โ ๊ธ์ง (ํ ์ค์ ๋ชฐ์์ฐ๊ธฐ โ ์ถ์ ๋ถ๊ฐ):
[slide 1]
์ฒซ ๋ฌธ์ฅ. [pause:400] ๋ ๋ฒ์งธ ๋ฌธ์ฅ. [pause:600] ์ธ ๋ฒ์งธ ๋ฌธ์ฅ.
โ
์ฌ๋ฐ๋ฆ (์ค ๋จ์ ๋ถํ โ ์ถ์ ๊ฐ๋ฅ):
[slide 1]
์ฒซ ๋ฌธ์ฅ.
[pause:400]
๋ ๋ฒ์งธ ๋ฌธ์ฅ.
[pause:600]
์ธ ๋ฒ์งธ ๋ฌธ์ฅ.
๊ท์น
๋ฌธ์ฅ ๊ธธ์ด
- ์์ด โค25 ๋จ์ด, ํ๊ตญ์ด โค20 ์ด์
- ํ ํธํก์ ์ฝ์ ์ ์๋ ๊ธธ์ด
์ถ์ฝยท๊ตฌ์ด์ฒด ํ์ฉ
- "๊ทธ๋์ ์ด๊ฒ ๋ญ๋๋ฉด", "์ด๊ฒ ๊ฝค ์ค์ํฉ๋๋ค"
- ๋๋ฌด ๋ฌธ์ด์ ํํ ์ง์ ("๊ธฐ์ธํ๋ค" โ "๋๋ฌธ์
๋๋ค")
Cue์ Marker
| ํ๊ทธ | ์ฉ๋ | ์ |
|---|
[slide N] | TTS ํ์ฒ๋ฆฌ ์ ์ค๋์ค ๊ตฌ๊ฐ ๋ถํ | [slide 3] |
[pause:ms] | ๋ฌธ์ฅ ์ฌ์ด ํธํก | [pause:400] |
[emph]...[/emph] | ๊ฐ์กฐ (SSML ๋ณํ ์ emphasis) | [emph]์ค์[/emph] |
pause ๊ถ์ฅ๊ฐ:
- ๋ฌธ์ฅ ์ฌ์ด: 300~400ms
- ๋ฌธ๋จ ์ ํ: 600~800ms
- ๊ฐ์กฐ/์ง๋ฌธ ํ: 800~1200ms
- ์ฌ๋ผ์ด๋ ์ ํ ํ ์ฒซ ๋ฌธ์ฅ ์: 500ms
Unspeakable ๋ณํ (ํต์ฌ)
Raw code literal โ ์ค๋ช
ํ
- โ
const x = () => { return 1; }
- โ
"const ํค์๋๋ก x๋ฅผ ์ ์ธํ๊ณ , ํ์ดํ ํจ์๋ก 1์ ๋ฐํํฉ๋๋ค"
๊ธด ์ซ์ โ ๊ตฌ์ด
- โ "3.14159265358979"
- โ
"ํ์ด, ์ฝ 3.14"
- โ "403,825๊ฐ"
- โ
"์ฝ 40๋ง ๊ฐ"
URL โ ์ด๋ฆ
์ฌ๋ณผยท์ด๋ชจ์ง โ ์ค๋ช
๋๋ ์ ๊ฑฐ
- โ "Use ๐ this"
- โ
"์ด๊ฒ์ ์ฌ์ฉํ์ธ์"
๊ธธ์ด ๊ณ์ฐ
๋ฌธ์/๋ถ ์๋๋ voice์ TTS ์์ง, speed ์ค์ ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋ค. ์๋ ์ค์ธก ํ ๊ธฐ๋ฐ:
Voice๋ณ ๋ฐํ ์๋ (char/sec, speed=1.0 ๊ธฐ์ค)
| ์์ง | Voice | ko ์ค์ธก | en ์ค์ธก |
|---|
OpenAI gpt-4o-mini-tts | nova | โ4.3 | โ3.8 (est.) |
OpenAI tts-1 / tts-1-hd | ๊ธฐ๋ณธ๊ฐ | โ4.2 | โ3.7 (est.) |
| edge-tts | ko-KR-SunHiNeural | โ4.3 | โ |
| edge-tts | en-US-*Neural | โ | โ3.5 |
2026-04-22 ์ค์ธก โ 13~14 ์ฌ๋ผ์ด๋ ๋จ์ ํ๋ณธ ๊ธฐ๋ฐ. ์ถ๊ฐ voice/์ธ์ด๋ ์คํ ์ ๋ณด๊ฐ.
Script ๊ธธ์ด ๊ณต์ (Bloom/affect์ ๊ด๊ณ์์ด duration ๋ชฉํ ์ญ์ฐ)
# ๋ณ์: rate_cps = ์ ํ์ char/sec, speed = TTS speed ํ๋ผ๋ฏธํฐ (๊ธฐ๋ณธ 1.0)
script_length_chars โ target_duration_sec ร rate_cps ร speed
# ์: ko nova speed=1.3, 10๋ถ class ๋ชฉํ
= 600 ร 4.3 ร 1.3 โ 3354์
pause ๊ตฌ๊ฐ์ ยฑ5~15% ์ถ๊ฐ๋ก ์ฐจ์งํ๋ ํ
์คํธ ๋ชฉํ๋ 97%~85%๋ก ๋ณด์์ ์ผ๋ก.
์ด์ ๊ณต์๊ณผ์ ์ฐจ์ด
v1์ ๋จ์ผ ๊ณต์(2.7 chars/sec)์ Neural voice์ ์ค์ ์๋(โ4.3)๋ฅผ ํฌ๊ฒ ๊ณผ์ํ๊ฐ โ ์คํฌ๋ฆฝํธ๊ฐ target๋ณด๋ค 60% ๊ธธ์ด์ง๋ ๊ฒฝํฅ ์์์. ํ๋ค์ค ์ด๊ธฐ ์คํ์์ estimate 1,127s vs ์ค์ธก 713s์ 36% ๊ฐญ์ผ๋ก ํ์ธ.
Prior-run calibration (์๋ ๋ณด์ , ๊ถ์ฅ)
๊ฐ์ ์ฃผ์ ยท์ธ์ด๋ก ์ฌ์คํํ ๋๋ ์ ์ rate_cps ํ
์ด๋ธ ๋์ ์ด์ run์ ์ค์ธก๊ฐ์ ์ฌ์ฉํด per-class๋ก ๋ณด์ ํ๋ค.
์
๋ ฅ: course_prev_*/manifest.json ์ sections[].classes[].actual_audio_duration_sec (synth-manifest.py ๊ฐ ๊ธฐ๋ก) + ํด๋น class์ duration_min target.
if prior_manifest exists and class_id matches:
prev_actual_sec = prev_class.actual_audio_duration_sec
prev_target_sec = prev_class.duration_min * 60
calib = prev_actual_sec / prev_target_sec # 1.08 = 8% over, 0.92 = 8% under
calib = max(0.7, min(1.4, calib)) # safety clamp โ ๊ทน๋จ๊ฐ ๋ฌด์
adjusted_char_budget = (target_duration_sec ร rate_cps ร speed) / calib
else:
adjusted_char_budget = target_duration_sec ร rate_cps ร speed
๋์ ์๋ฆฌ:
- ์ด์ run์ด target๋ณด๋ค ๊ธธ์์ผ๋ฉด (calib > 1) ๋ค์ run์ char budget์ ์ค์ธ๋ค.
- ์งง์์ผ๋ฉด ๋๋ฆฐ๋ค.
- clamp
[0.7, 1.4] ๋ rate_cps ํ
์ด๋ธ ์์ฒด๊ฐ ํฌ๊ฒ ํ๋ฆฐ ์ด๊ธฐ run์์ ๊ณผ๋ณด์ ํญ์ฃผ ๋ฐฉ์ง์ฉ.
๋ณด์ ์คํจ ๋์ (fallback)
prior_manifest๊ฐ ์๊ฑฐ๋ class_id ๋งค์นญ ์คํจ ์, ๋๋ calib ์ ์ฉ ํ์๋ ์ค์ duration์ด target ยฑ10% ๋ฒ์ด๋๋ฉด:
- ๋๋ฌด ์งง์: beat์
key_points์ ๊ตฌ์ฒด ์์ 1๊ฐ ์ถ๊ฐ
- ๋๋ฌด ๊น: ์ค๋ณต ๋ถ์ฐ ์ ๊ฑฐ, teach beat๋ฅผ 2๊ฐ๋ก ๋ถํ ํ ๋ค ๊ฐ ๋ถ๋ ์ถ์
ยฑ10% ์ด๋ด ๋ง์ถค.
[slide N] ๋งคํ
slide.source.md์ ์ฌ๋ผ์ด๋ ๋ฒํธ ์์์ ์ ํํ ์ผ์นํด์ผ ํ๋ค.
- ์ฌ๋ผ์ด๋ 1 = ์ ๋ชฉ ์ฌ๋ผ์ด๋ (์ธ๊ธํ๊ณ ๋์ด๊ฐ, 20~30s)
- ์ฌ๋ผ์ด๋ N = recap (๊ฐ์กฐํ๋ฉฐ ๋ง๋ฌด๋ฆฌ)
coherence-reviewer๋ ์ด ๋งคํ์ ๊ฒ์ฆํ๋ค.
SSML ๋ณํ
์ฌ์ฉ์๊ฐ ์์ฒญํ๊ฑฐ๋ language != ko์ผ ๋ transcript.ssml ์ถ๊ฐ ์์ฑ.
<speak xmlns="http://www.w3.org/2001/10/synthesis" version="1.1" xml:lang="ko-KR">
<p>
<s>์ฒซ ๋ฌธ์ฅ์
๋๋ค.</s>
<break time="400ms"/>
<s>๋ค์ ๋ฌธ์ฅ์ ์กฐ๊ธ <emphasis level="moderate">๊ฐ์กฐ</emphasis>ํฉ๋๋ค.</s>
</p>
</speak>
<speak> ๋ฃจํธ, xml:lang ํ์
[pause:X] โ <break time="Xms"/>
[emph]X[/emph] โ <emphasis level="moderate">X</emphasis>
[slide N] โ SSML์์๋ <mark name="slide_N"/>๋ก ๋ณํ
scripts/validate-ssml.sh๋ก W3C SSML 1.1 ์คํค๋ง ๊ฒ์ฆ.
Speaker affect ๋ฐ์
class-planner์ speaker_affect ํ๋๋ฅผ ๋ฌธ์ฒดยท์๋์ ๋ฐ์:
ํธ๊ธฐ์ฌ: ์ง๋ฌธ ๋ ์ต์, ์งง์ ๋ฌธ์ฅ
๋จํธ: ๋ช
์ฌํ ์ข
๊ฒฐ, ์ค๊ฐ pause ์ต์
์น๊ทผ: ์ถ์ฝ ์ ๊ทน, ๊ฐํ์ฌ OK
์์: ์ฅ๋ฌธ, ์์์ด ์ ์
์ฌ์คํ ๊ท์น
- slide ์ ๋ณ๊ฒฝ๋๋ฉด [slide N] ์ ์ฒด ์ฌ๋งคํ
- ํค ๋ณ๊ฒฝ์ ์ ์ฒด ์ฌ์์ฑ (๋ถ๋ถ ๊ต์ฒดํ๋ฉด ์ํ๊ฐ)
์ฒดํฌ๋ฆฌ์คํธ