feat(web-test): per-caption voice + speechRate for multi-voice narration

- addNarration: use cap.voice override per caption (fallback to global)
- showCaption/showImage/showTitleSlide: pass opts.voice to caption entry
- showCaption: record caption when text is empty but speech is explicit
- startRecording: add speechRate option (default 70ms/char, 85 for ElevenLabs)
- run.mjs: increase exec timeout to 30min for long recordings
- docs: update recording.md and web-test-recording-guide.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-03-21 16:30:02 +03:00
parent ca0dac2693
commit 6f36e36166
4 changed files with 59 additions and 17 deletions
+36 -2
View File
@@ -123,12 +123,12 @@ Claude вызовет `addNarration` с другим голосом. Текст
"videoTimestamps": true,
"captions": [
{ "text": "Переходим в раздел «Продажи»", "speech": "Переходим в раздел Продажи", "time": 3160 },
{ "text": "Открываем заказы клиентов", "speech": "Открываем заказы клиентов", "time": 7040 }
{ "text": "Открываем заказы клиентов", "speech": "Открываем заказы клиентов", "time": 7040, "voice": "bqbHGIIO5oETYIqhWmfk" }
]
}
```
Можно отредактировать `speech` (текст озвучки) и переозвучить:
Можно отредактировать `speech` (текст озвучки) или добавить `voice` (голос для конкретной реплики) и переозвучить:
```
> Отредактируй субтитры в recordings/demo.captions.json — замени "Продажи" на
@@ -204,6 +204,36 @@ await clickElement('Создать');
await wait(2); // форма загружается
```
### Два голоса (подкаст / диалог)
Параметр `voice` в `showCaption` задаёт голос для конкретной реплики. `addNarration` автоматически использует его вместо глобального:
```js
const MALE = 'bqbHGIIO5oETYIqhWmfk'; // Alexander
const FEMALE = '0ArNnoIAWKlT4WweaVMY'; // Elena Gromova
// speechRate: 85 — ElevenLabs медленнее Edge TTS, нужен запас
await startRecording('podcast.mp4', { speechRate: 85 });
await showImage('slides/slide-01.png', { style: 'full', speech: false });
await showCaption('', { speech: 'Привет! Сегодня поговорим...', voice: MALE });
await wait(0.8);
await showCaption('', { speech: 'А я буду задавать вопросы...', voice: FEMALE });
await wait(0.8);
const video = await stopRecording();
const result = await addNarration(video.file, {
provider: 'elevenlabs',
apiKey: 'sk_...',
// глобальный voice не нужен — каждый caption несёт свой
});
```
Приёмы:
- `showCaption('', { speech, voice })` — пустой текст (без субтитра на экране), но speech записывается для озвучки
- `showImage` со `speech: false` — слайд без озвучки, реплики идут через `showCaption`
- `speechRate: 85` — для ElevenLabs увеличиваем множитель (по умолчанию 70мс/символ), чтобы фразы не наезжали друг на друга
### Разделение текста и озвучки
Параметр `speech` в `showCaption` позволяет показывать одно, а озвучивать другое:
@@ -267,6 +297,9 @@ await showCaption('Технические детали', { speech: false });
| Olga Orlova | `d60rsXo2p0OwikDR5bS7` | Clear and Engaging |
| Artem | `WTn2eCRCpoFAC50VD351` | Friendly & Professional |
| Denis | `0BcDz9UPwL3MpsnTeUlO` | Pleasant, Engaging and Friendly |
| Alexander | `bqbHGIIO5oETYIqhWmfk` | Pleasant, Warm and Natural |
| Elena Gromova | `0ArNnoIAWKlT4WweaVMY` | Podcasts & Conversation |
| Victor | `9fjVd0EYNNXHllJquVdT` | Moscow accent |
```json
{
@@ -345,6 +378,7 @@ const narrated = await addNarration(video.file, {
| `No captions available` | Используйте `showCaption()` во время записи |
| TTS timeout | Проверьте интернет-соединение (Edge TTS требует сеть) |
| Озвучка обрезается | Увеличьте паузы `wait()` между субтитрами |
| Фразы наезжают друг на друга | Увеличьте `speechRate` в `startRecording` (85 для ElevenLabs) |
## Связанные навыки