@kortix/sandbox 0.4.1
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/config/customize.sh +143 -0
- package/config/kortix-env-setup.sh +25 -0
- package/kortix-master/package.json +22 -0
- package/kortix-master/src/config.ts +22 -0
- package/kortix-master/src/index.ts +44 -0
- package/kortix-master/src/routes/env.ts +65 -0
- package/kortix-master/src/routes/proxy.ts +108 -0
- package/kortix-master/src/routes/update.ts +185 -0
- package/kortix-master/src/services/proxy.ts +43 -0
- package/kortix-master/src/services/secret-store.ts +156 -0
- package/kortix-master/tsconfig.json +14 -0
- package/opencode/agents/kortix-browser.md +142 -0
- package/opencode/agents/kortix-build.md +62 -0
- package/opencode/agents/kortix-explore.md +66 -0
- package/opencode/agents/kortix-image-gen.md +33 -0
- package/opencode/agents/kortix-main.md +450 -0
- package/opencode/agents/kortix-plan.md +100 -0
- package/opencode/agents/kortix-research.md +84 -0
- package/opencode/agents/kortix-sheets.md +61 -0
- package/opencode/agents/kortix-slides.md +64 -0
- package/opencode/agents/kortix-web-dev.md +572 -0
- package/opencode/commands/email.md +36 -0
- package/opencode/commands/init.md +43 -0
- package/opencode/commands/journal.md +44 -0
- package/opencode/commands/memory-init.md +81 -0
- package/opencode/commands/memory-search.md +50 -0
- package/opencode/commands/memory-status.md +56 -0
- package/opencode/commands/research.md +36 -0
- package/opencode/commands/search.md +38 -0
- package/opencode/commands/slides.md +32 -0
- package/opencode/commands/spreadsheet.md +30 -0
- package/opencode/memory.json +37 -0
- package/opencode/ocx.jsonc +10 -0
- package/opencode/opencode.jsonc +103 -0
- package/opencode/package.json +25 -0
- package/opencode/patches/apply.sh +19 -0
- package/opencode/patches/opencode-pty-spawn.txt +49 -0
- package/opencode/plugin/background-agents.ts.disabled +483 -0
- package/opencode/plugin/kdco-primitives/get-project-id.ts +172 -0
- package/opencode/plugin/kdco-primitives/index.ts +26 -0
- package/opencode/plugin/kdco-primitives/log-warn.ts +51 -0
- package/opencode/plugin/kdco-primitives/mutex.ts +122 -0
- package/opencode/plugin/kdco-primitives/shell.ts +138 -0
- package/opencode/plugin/kdco-primitives/temp.ts +36 -0
- package/opencode/plugin/kdco-primitives/terminal-detect.ts +34 -0
- package/opencode/plugin/kdco-primitives/types.ts +13 -0
- package/opencode/plugin/kdco-primitives/with-timeout.ts +84 -0
- package/opencode/plugin/memory.ts +306 -0
- package/opencode/plugin/worktree/state.ts +412 -0
- package/opencode/plugin/worktree/terminal.ts +1002 -0
- package/opencode/plugin/worktree.ts +861 -0
- package/opencode/skills/KORTIX-browser/SKILL.md +478 -0
- package/opencode/skills/KORTIX-cron-triggers/SKILL.md +173 -0
- package/opencode/skills/KORTIX-deep-research/SKILL.md +278 -0
- package/opencode/skills/KORTIX-docx/SKILL.md +398 -0
- package/opencode/skills/KORTIX-docx/scripts/__init__.py +1 -0
- package/opencode/skills/KORTIX-docx/scripts/accept_changes.py +104 -0
- package/opencode/skills/KORTIX-docx/scripts/comment.py +244 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/__init__.py +0 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/merge_runs.py +199 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/opencode/skills/KORTIX-docx/scripts/office/pack.py +159 -0
- package/opencode/skills/KORTIX-docx/scripts/office/soffice.py +183 -0
- package/opencode/skills/KORTIX-docx/scripts/office/unpack.py +132 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validate.py +111 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/__init__.py +15 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/base.py +847 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/docx.py +446 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/pptx.py +275 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/redlining.py +247 -0
- package/opencode/skills/KORTIX-docx/scripts/render_docx.py +179 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/comments.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtended.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtensible.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsIds.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/people.xml +3 -0
- package/opencode/skills/KORTIX-domain-research/SKILL.md +96 -0
- package/opencode/skills/KORTIX-domain-research/scripts/domain-lookup.py +810 -0
- package/opencode/skills/KORTIX-elevenlabs/SKILL.md +230 -0
- package/opencode/skills/KORTIX-elevenlabs/scripts/tts.py +389 -0
- package/opencode/skills/KORTIX-email/SKILL.md +145 -0
- package/opencode/skills/KORTIX-legal-writer/SKILL.md +409 -0
- package/opencode/skills/KORTIX-legal-writer/references/bluebook.md +152 -0
- package/opencode/skills/KORTIX-legal-writer/references/document-types.md +416 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/courtlistener.py +291 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/ecfr_lookup.py +299 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/verify-legal.py +507 -0
- package/opencode/skills/KORTIX-logo-creator/SKILL.md +293 -0
- package/opencode/skills/KORTIX-logo-creator/references/prompt-patterns.md +134 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/compose_logo.py +406 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/create_logo_sheet.py +258 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/remove_bg.py +96 -0
- package/opencode/skills/KORTIX-memory/SKILL.md +261 -0
- package/opencode/skills/KORTIX-memory/scripts/export-sessions.py +409 -0
- package/opencode/skills/KORTIX-paper-creator/SKILL.md +549 -0
- package/opencode/skills/KORTIX-paper-creator/assets/template.tex +101 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/compile.sh +177 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/openalex_to_bibtex.py +220 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/verify.sh +354 -0
- package/opencode/skills/KORTIX-paper-search/SKILL.md +418 -0
- package/opencode/skills/KORTIX-pdf/SKILL.md +232 -0
- package/opencode/skills/KORTIX-pdf/forms.md +36 -0
- package/opencode/skills/KORTIX-pdf/reference.md +105 -0
- package/opencode/skills/KORTIX-pdf/scripts/check_bounding_boxes.py +65 -0
- package/opencode/skills/KORTIX-pdf/scripts/check_fillable_fields.py +11 -0
- package/opencode/skills/KORTIX-pdf/scripts/convert_pdf_to_images.py +33 -0
- package/opencode/skills/KORTIX-pdf/scripts/create_validation_image.py +37 -0
- package/opencode/skills/KORTIX-pdf/scripts/extract_form_field_info.py +122 -0
- package/opencode/skills/KORTIX-pdf/scripts/extract_form_structure.py +115 -0
- package/opencode/skills/KORTIX-pdf/scripts/fill_fillable_fields.py +98 -0
- package/opencode/skills/KORTIX-pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/opencode/skills/KORTIX-plan/SKILL.md +228 -0
- package/opencode/skills/KORTIX-presentation-viewer/SKILL.md +87 -0
- package/opencode/skills/KORTIX-presentation-viewer/serve.ts +136 -0
- package/opencode/skills/KORTIX-presentation-viewer/viewer.html +559 -0
- package/opencode/skills/KORTIX-presentations/SKILL.md +344 -0
- package/opencode/skills/KORTIX-remotion/SKILL.md +56 -0
- package/opencode/skills/KORTIX-remotion/rules/3d.md +86 -0
- package/opencode/skills/KORTIX-remotion/rules/animations.md +29 -0
- package/opencode/skills/KORTIX-remotion/rules/assets.md +78 -0
- package/opencode/skills/KORTIX-remotion/rules/audio-visualization.md +198 -0
- package/opencode/skills/KORTIX-remotion/rules/audio.md +169 -0
- package/opencode/skills/KORTIX-remotion/rules/calculate-metadata.md +104 -0
- package/opencode/skills/KORTIX-remotion/rules/can-decode.md +75 -0
- package/opencode/skills/KORTIX-remotion/rules/charts.md +120 -0
- package/opencode/skills/KORTIX-remotion/rules/compositions.md +141 -0
- package/opencode/skills/KORTIX-remotion/rules/display-captions.md +184 -0
- package/opencode/skills/KORTIX-remotion/rules/extract-frames.md +229 -0
- package/opencode/skills/KORTIX-remotion/rules/ffmpeg.md +38 -0
- package/opencode/skills/KORTIX-remotion/rules/fonts.md +152 -0
- package/opencode/skills/KORTIX-remotion/rules/get-audio-duration.md +58 -0
- package/opencode/skills/KORTIX-remotion/rules/get-video-dimensions.md +68 -0
- package/opencode/skills/KORTIX-remotion/rules/get-video-duration.md +58 -0
- package/opencode/skills/KORTIX-remotion/rules/gifs.md +141 -0
- package/opencode/skills/KORTIX-remotion/rules/images.md +130 -0
- package/opencode/skills/KORTIX-remotion/rules/import-srt-captions.md +69 -0
- package/opencode/skills/KORTIX-remotion/rules/light-leaks.md +73 -0
- package/opencode/skills/KORTIX-remotion/rules/lottie.md +68 -0
- package/opencode/skills/KORTIX-remotion/rules/maps.md +401 -0
- package/opencode/skills/KORTIX-remotion/rules/measuring-dom-nodes.md +35 -0
- package/opencode/skills/KORTIX-remotion/rules/measuring-text.md +143 -0
- package/opencode/skills/KORTIX-remotion/rules/parameters.md +98 -0
- package/opencode/skills/KORTIX-remotion/rules/sequencing.md +118 -0
- package/opencode/skills/KORTIX-remotion/rules/subtitles.md +36 -0
- package/opencode/skills/KORTIX-remotion/rules/tailwind.md +11 -0
- package/opencode/skills/KORTIX-remotion/rules/text-animations.md +20 -0
- package/opencode/skills/KORTIX-remotion/rules/timing.md +179 -0
- package/opencode/skills/KORTIX-remotion/rules/transcribe-captions.md +70 -0
- package/opencode/skills/KORTIX-remotion/rules/transitions.md +197 -0
- package/opencode/skills/KORTIX-remotion/rules/transparent-videos.md +106 -0
- package/opencode/skills/KORTIX-remotion/rules/trimming.md +53 -0
- package/opencode/skills/KORTIX-remotion/rules/videos.md +171 -0
- package/opencode/skills/KORTIX-secrets/SKILL.md +280 -0
- package/opencode/skills/KORTIX-semantic-search/SKILL.md +213 -0
- package/opencode/skills/KORTIX-session-search/SKILL.md +807 -0
- package/opencode/skills/KORTIX-session-search/Untitled +1 -0
- package/opencode/skills/KORTIX-skill-creator/SKILL.md +163 -0
- package/opencode/skills/KORTIX-web-research/SKILL.md +69 -0
- package/opencode/skills/KORTIX-xlsx/LICENSE.txt +30 -0
- package/opencode/skills/KORTIX-xlsx/SKILL.md +549 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/__init__.py +0 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/merge_runs.py +199 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/pack.py +159 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/soffice.py +183 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/unpack.py +132 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validate.py +111 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/__init__.py +15 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/base.py +847 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/docx.py +446 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/pptx.py +275 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/redlining.py +247 -0
- package/opencode/skills/KORTIX-xlsx/scripts/recalc.py +184 -0
- package/opencode/tools/image-gen.ts +342 -0
- package/opencode/tools/image-search.ts +190 -0
- package/opencode/tools/memory-get.ts +168 -0
- package/opencode/tools/memory-search.ts +247 -0
- package/opencode/tools/presentation-gen.ts +723 -0
- package/opencode/tools/scrape-webpage.ts +115 -0
- package/opencode/tools/scripts/.python-version +1 -0
- package/opencode/tools/scripts/convert_pdf.py +184 -0
- package/opencode/tools/scripts/convert_pptx.py +562 -0
- package/opencode/tools/scripts/pyproject.toml +11 -0
- package/opencode/tools/scripts/uv.lock +287 -0
- package/opencode/tools/scripts/validate_slide.py +74 -0
- package/opencode/tools/show-user.ts +217 -0
- package/opencode/tools/tests/e2e-presentation-fix.ts +277 -0
- package/opencode/tools/tests/image-gen.test.ts +215 -0
- package/opencode/tools/tests/image-search.test.ts +125 -0
- package/opencode/tools/tests/memory-system-benchmark.ts +1076 -0
- package/opencode/tools/tests/presentation-gen.test.ts +389 -0
- package/opencode/tools/tests/scrape-webpage.test.ts +74 -0
- package/opencode/tools/tests/show-user.test.ts +241 -0
- package/opencode/tools/tests/video-gen.test.ts +110 -0
- package/opencode/tools/tests/web-search.test.ts +106 -0
- package/opencode/tools/video-gen.ts +200 -0
- package/opencode/tools/web-search.ts +153 -0
- package/opencode/tsconfig.json +29 -0
- package/package.json +36 -0
- package/patch-agent-browser.js +100 -0
- package/postinstall.sh +88 -0
- package/services/KORTIX-presentation-viewer/run +37 -0
- package/services/agent-browser-viewer/run +48 -0
- package/services/kortix-master/run +16 -0
- package/services/lss-sync/run +22 -0
- package/services/opencode-serve/run +25 -0
- package/services/opencode-web/run +21 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kortix-elevenlabs
|
|
3
|
+
description: "ElevenLabs audio generation — text-to-speech, voice cloning, and sound effects. Use this skill any time the agent needs to: convert text to spoken audio, narrate documents or content, generate voiceovers, clone voices from audio samples, create sound effects, or produce any audio output from text. Supports multiple voices, languages, models, voice cloning, batch processing, and sound effect generation. Requires ELEVENLABS_API_KEY."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ElevenLabs — Text-to-Speech, Voice Cloning & Sound Effects
|
|
7
|
+
|
|
8
|
+
ElevenLabs-powered audio generation. Convert any text to natural-sounding speech, clone voices, generate sound effects.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
**Required env var:** `ELEVENLABS_API_KEY`
|
|
15
|
+
|
|
16
|
+
The CLI script at `scripts/tts.py` uses only Python stdlib (`urllib`, `json`, `argparse`) — no pip dependencies needed.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Quick Reference
|
|
21
|
+
|
|
22
|
+
All commands use the CLI script:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python skills/KORTIX-tts/scripts/tts.py <command> [args]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Speak — Convert text to speech
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Basic — default voice (George), multilingual v2 model
|
|
32
|
+
python scripts/tts.py speak "Hello, this is Kortix speaking."
|
|
33
|
+
|
|
34
|
+
# Named voice
|
|
35
|
+
python scripts/tts.py speak "Welcome to the presentation." --voice Rachel
|
|
36
|
+
|
|
37
|
+
# Custom output file
|
|
38
|
+
python scripts/tts.py speak "Chapter one." --voice George -o chapter1.mp3
|
|
39
|
+
|
|
40
|
+
# From a file (prefix with @)
|
|
41
|
+
python scripts/tts.py speak @article.txt -o narration.mp3
|
|
42
|
+
|
|
43
|
+
# From stdin
|
|
44
|
+
echo "Dynamic text" | python scripts/tts.py speak -
|
|
45
|
+
|
|
46
|
+
# With voice tuning
|
|
47
|
+
python scripts/tts.py speak "Dramatic reading." --voice Rachel --stability 0.3 --similarity 0.9 --style 0.7
|
|
48
|
+
|
|
49
|
+
# High quality output
|
|
50
|
+
python scripts/tts.py speak "Studio quality." --format mp3_44100_192
|
|
51
|
+
|
|
52
|
+
# Different model (faster, English-only)
|
|
53
|
+
python scripts/tts.py speak "Quick response." --model eleven_turbo_v2_5
|
|
54
|
+
|
|
55
|
+
# Speed control
|
|
56
|
+
python scripts/tts.py speak "Slowly now." --speed 0.7
|
|
57
|
+
python scripts/tts.py speak "Fast paced!" --speed 1.5
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Voices — List and search
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# List all available voices
|
|
64
|
+
python scripts/tts.py voices
|
|
65
|
+
|
|
66
|
+
# Search by name, gender, accent, or use case
|
|
67
|
+
python scripts/tts.py voices --search "female"
|
|
68
|
+
python scripts/tts.py voices --search "british"
|
|
69
|
+
python scripts/tts.py voices --search "narration"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Models — List available TTS models
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
python scripts/tts.py models
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Clone — Create a custom voice from audio samples
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Clone from audio files (1-25 samples, each 1-10 minutes)
|
|
82
|
+
python scripts/tts.py clone "ClientVoice" sample1.mp3 sample2.mp3
|
|
83
|
+
|
|
84
|
+
# With description
|
|
85
|
+
python scripts/tts.py clone "CEO" ceo_speech.mp3 --description "Confident male voice, American accent"
|
|
86
|
+
|
|
87
|
+
# Use the cloned voice
|
|
88
|
+
python scripts/tts.py speak "Hello from my cloned voice." --voice-id <returned_voice_id>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Batch — Convert entire documents
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Convert a text file to a single audio file
|
|
95
|
+
python scripts/tts.py batch article.txt -o article_audio/
|
|
96
|
+
|
|
97
|
+
# Split by paragraphs — one audio file per paragraph
|
|
98
|
+
python scripts/tts.py batch book_chapter.txt --split-paragraphs -o chapter_audio/
|
|
99
|
+
|
|
100
|
+
# With specific voice
|
|
101
|
+
python scripts/tts.py batch script.txt --voice Rachel --split-paragraphs
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Sound Effects — Generate from text prompts
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Generate a sound effect
|
|
108
|
+
python scripts/tts.py sound "ocean waves crashing on a beach"
|
|
109
|
+
|
|
110
|
+
# With specific output and duration
|
|
111
|
+
python scripts/tts.py sound "thunderstorm with heavy rain" -o thunder.mp3 --duration 10.0
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Voice Settings Guide
|
|
117
|
+
|
|
118
|
+
Fine-tune voice output with these parameters:
|
|
119
|
+
|
|
120
|
+
| Parameter | Range | Default | Effect |
|
|
121
|
+
|---|---|---|---|
|
|
122
|
+
| `--stability` | 0.0 - 1.0 | 0.5 | Higher = more consistent, lower = more expressive/varied |
|
|
123
|
+
| `--similarity` | 0.0 - 1.0 | 0.75 | Higher = closer to original voice, lower = more creative |
|
|
124
|
+
| `--style` | 0.0 - 1.0 | 0.0 | Higher = more expressive style, can reduce stability |
|
|
125
|
+
| `--speed` | 0.5 - 2.0 | 1.0 | Playback speed multiplier |
|
|
126
|
+
|
|
127
|
+
**Recommended presets:**
|
|
128
|
+
|
|
129
|
+
- **Narration/Audiobook:** `--stability 0.5 --similarity 0.75` (balanced, natural)
|
|
130
|
+
- **News/Formal:** `--stability 0.8 --similarity 0.8` (consistent, clear)
|
|
131
|
+
- **Character/Dramatic:** `--stability 0.3 --similarity 0.8 --style 0.7` (expressive, varied)
|
|
132
|
+
- **Conversational:** `--stability 0.4 --similarity 0.6` (natural variation)
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Output Formats
|
|
137
|
+
|
|
138
|
+
| Format | Quality | Size | Use Case |
|
|
139
|
+
|---|---|---|---|
|
|
140
|
+
| `mp3_44100_128` | High (default) | Medium | General purpose, good quality |
|
|
141
|
+
| `mp3_44100_192` | Very high | Large | Studio quality, archival |
|
|
142
|
+
| `mp3_22050_32` | Low | Small | Voice messages, previews |
|
|
143
|
+
| `pcm_44100` | Lossless | Very large | Post-processing, editing |
|
|
144
|
+
| `pcm_16000` | Lossless low | Large | Speech recognition input |
|
|
145
|
+
| `opus_48000_128` | High | Small | Web streaming, efficient |
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Models
|
|
150
|
+
|
|
151
|
+
| Model | Speed | Quality | Languages | Best For |
|
|
152
|
+
|---|---|---|---|---|
|
|
153
|
+
| `eleven_multilingual_v2` | Normal | Highest | 29 languages | Default — best quality, multilingual |
|
|
154
|
+
| `eleven_turbo_v2_5` | Fast | High | 32 languages | Low-latency, near-instant generation |
|
|
155
|
+
| `eleven_monolingual_v1` | Normal | Good | English only | Legacy English-only workloads |
|
|
156
|
+
|
|
157
|
+
Always use `eleven_multilingual_v2` unless speed is critical (then use `eleven_turbo_v2_5`).
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Common Workflows
|
|
162
|
+
|
|
163
|
+
### Narrate a document
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# Read the document, generate speech
|
|
167
|
+
python scripts/tts.py speak @workspace/report.md --voice Rachel -o report_narration.mp3
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Create a podcast intro
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
python scripts/tts.py speak "Welcome to the Kortix Weekly. I'm your host, and today we're diving into autonomous AI agents." \
|
|
174
|
+
--voice George --stability 0.4 --similarity 0.8 --style 0.5 \
|
|
175
|
+
-o podcast_intro.mp3
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Narrate a presentation (per-slide)
|
|
179
|
+
|
|
180
|
+
For each slide, generate a separate audio file:
|
|
181
|
+
```bash
|
|
182
|
+
python scripts/tts.py speak "Slide 1: Introduction to our company" --voice Rachel -o slides/01.mp3
|
|
183
|
+
python scripts/tts.py speak "Slide 2: Our key metrics this quarter" --voice Rachel -o slides/02.mp3
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Or write all narration to a text file (one paragraph per slide) and batch it:
|
|
187
|
+
```bash
|
|
188
|
+
python scripts/tts.py batch slide_notes.txt --split-paragraphs --voice Rachel -o slide_audio/
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Voice clone for personalization
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# Clone the user's voice from samples they provide
|
|
195
|
+
python scripts/tts.py clone "UserVoice" sample1.mp3 sample2.mp3 sample3.mp3 \
|
|
196
|
+
--description "The user's natural speaking voice"
|
|
197
|
+
|
|
198
|
+
# Use it for all future TTS
|
|
199
|
+
python scripts/tts.py speak "Personalized message." --voice-id <voice_id> -o message.mp3
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Generate ambient audio
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
python scripts/tts.py sound "coffee shop ambiance with gentle chatter" -o ambient.mp3 --duration 15
|
|
206
|
+
python scripts/tts.py sound "gentle rain on a window" -o rain.mp3 --duration 30
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Integration Notes
|
|
212
|
+
|
|
213
|
+
- **No pip dependencies.** The script uses only Python stdlib (`urllib.request`, `json`, `argparse`). Works on any Python 3.10+ installation.
|
|
214
|
+
- **Output files** are saved relative to the current working directory. Use `-o` to specify exact paths.
|
|
215
|
+
- **Long text** is handled automatically by the API. For very long documents (>5000 chars), consider using `batch` with `--split-paragraphs` for better quality and to avoid timeouts.
|
|
216
|
+
- **Rate limits** apply per your ElevenLabs plan. The script will return API errors if limits are hit.
|
|
217
|
+
- **Character usage** counts against your ElevenLabs monthly quota. Check your plan's limits.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Env Vars
|
|
222
|
+
|
|
223
|
+
| Variable | Required | Description |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| `ELEVENLABS_API_KEY` | Yes | Your ElevenLabs API key (also accepts `ELEVEN_API_KEY`) |
|
|
226
|
+
|
|
227
|
+
Add to `sandbox/.env` and `sandbox/opencode/.env`:
|
|
228
|
+
```
|
|
229
|
+
ELEVENLABS_API_KEY=your_key_here
|
|
230
|
+
```
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Kortix TTS — ElevenLabs Text-to-Speech CLI
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python tts.py speak "Hello world" # Speak with default voice
|
|
7
|
+
python tts.py speak "Hello world" --voice Rachel # Speak with named voice
|
|
8
|
+
python tts.py speak "Hello world" --voice-id JBFqnC... # Speak with voice ID
|
|
9
|
+
python tts.py speak "Hello world" -o output.mp3 # Save to specific file
|
|
10
|
+
python tts.py speak "Hello world" --model eleven_turbo_v2_5 # Use specific model
|
|
11
|
+
python tts.py speak "Hello world" --format mp3_44100_192 # High quality output
|
|
12
|
+
python tts.py speak "Hello world" --stability 0.8 --similarity 0.9 # Custom settings
|
|
13
|
+
|
|
14
|
+
python tts.py voices # List all available voices
|
|
15
|
+
python tts.py voices --search "deep male" # Search voices
|
|
16
|
+
python tts.py models # List available models
|
|
17
|
+
|
|
18
|
+
python tts.py clone "MyVoice" sample1.mp3 sample2.mp3 # Clone a voice from samples
|
|
19
|
+
python tts.py clone "MyVoice" sample1.mp3 --description "A warm male voice"
|
|
20
|
+
|
|
21
|
+
python tts.py batch input.txt -o output_dir/ # Convert text file to speech
|
|
22
|
+
python tts.py batch input.txt --split-paragraphs # Split by paragraphs, one file each
|
|
23
|
+
|
|
24
|
+
python tts.py sound "ocean waves crashing" # Generate sound effect
|
|
25
|
+
python tts.py sound "thunder rumble" -o thunder.mp3 # Sound effect to file
|
|
26
|
+
|
|
27
|
+
Env: ELEVENLABS_API_KEY (required)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import sys
|
|
34
|
+
import urllib.request
|
|
35
|
+
import urllib.error
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
API_BASE = "https://api.elevenlabs.io/v1"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_api_key() -> str:
|
|
42
|
+
key = os.environ.get("ELEVENLABS_API_KEY") or os.environ.get("ELEVEN_API_KEY")
|
|
43
|
+
if not key:
|
|
44
|
+
print("ERROR: ELEVENLABS_API_KEY environment variable not set", file=sys.stderr)
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
return key
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def api_request(method: str, path: str, body: dict | None = None, stream: bool = False) -> bytes | dict:
|
|
50
|
+
"""Make an API request to ElevenLabs."""
|
|
51
|
+
key = get_api_key()
|
|
52
|
+
url = f"{API_BASE}{path}"
|
|
53
|
+
headers = {
|
|
54
|
+
"xi-api-key": key,
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
data = json.dumps(body).encode() if body else None
|
|
59
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
63
|
+
if stream:
|
|
64
|
+
return resp.read()
|
|
65
|
+
return json.loads(resp.read().decode())
|
|
66
|
+
except urllib.error.HTTPError as e:
|
|
67
|
+
error_body = e.read().decode()
|
|
68
|
+
try:
|
|
69
|
+
error_json = json.loads(error_body)
|
|
70
|
+
detail = error_json.get("detail", {})
|
|
71
|
+
if isinstance(detail, dict):
|
|
72
|
+
msg = detail.get("message", error_body)
|
|
73
|
+
else:
|
|
74
|
+
msg = str(detail)
|
|
75
|
+
except Exception:
|
|
76
|
+
msg = error_body
|
|
77
|
+
print(f"ERROR [{e.code}]: {msg}", file=sys.stderr)
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ── Commands ─────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def cmd_voices(args: argparse.Namespace) -> None:
|
|
85
|
+
"""List available voices."""
|
|
86
|
+
resp = api_request("GET", "/voices")
|
|
87
|
+
voices = resp.get("voices", [])
|
|
88
|
+
|
|
89
|
+
if args.search:
|
|
90
|
+
query = args.search.lower()
|
|
91
|
+
voices = [v for v in voices if
|
|
92
|
+
query in v.get("name", "").lower() or
|
|
93
|
+
query in (v.get("description") or "").lower() or
|
|
94
|
+
query in (v.get("labels", {}).get("accent", "") or "").lower() or
|
|
95
|
+
query in (v.get("labels", {}).get("gender", "") or "").lower() or
|
|
96
|
+
query in (v.get("labels", {}).get("use_case", "") or "").lower()]
|
|
97
|
+
|
|
98
|
+
if not voices:
|
|
99
|
+
print("No voices found.")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
print(f"{'Name':<25} {'ID':<25} {'Gender':<10} {'Accent':<15} {'Use Case':<15}")
|
|
103
|
+
print("-" * 90)
|
|
104
|
+
for v in voices:
|
|
105
|
+
labels = v.get("labels", {})
|
|
106
|
+
print(f"{v['name']:<25} {v['voice_id']:<25} {labels.get('gender', '-'):<10} "
|
|
107
|
+
f"{labels.get('accent', '-'):<15} {labels.get('use_case', '-'):<15}")
|
|
108
|
+
|
|
109
|
+
print(f"\nTotal: {len(voices)} voices")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def cmd_models(args: argparse.Namespace) -> None:
|
|
113
|
+
"""List available models."""
|
|
114
|
+
resp = api_request("GET", "/models")
|
|
115
|
+
models = resp if isinstance(resp, list) else resp.get("models", resp)
|
|
116
|
+
|
|
117
|
+
print(f"{'Model ID':<35} {'Name':<35} {'Languages':<10}")
|
|
118
|
+
print("-" * 80)
|
|
119
|
+
for m in models:
|
|
120
|
+
if not m.get("can_do_text_to_speech", True):
|
|
121
|
+
continue
|
|
122
|
+
langs = len(m.get("languages", []))
|
|
123
|
+
print(f"{m['model_id']:<35} {m['name']:<35} {langs:<10}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def resolve_voice_id(voice_name: str) -> str:
|
|
127
|
+
"""Resolve a voice name to a voice ID. If already an ID, return as-is."""
|
|
128
|
+
# If it looks like a voice ID (long alphanumeric), return directly
|
|
129
|
+
if len(voice_name) > 15 and voice_name.isalnum():
|
|
130
|
+
return voice_name
|
|
131
|
+
|
|
132
|
+
resp = api_request("GET", "/voices")
|
|
133
|
+
voices = resp.get("voices", [])
|
|
134
|
+
for v in voices:
|
|
135
|
+
if v["name"].lower() == voice_name.lower():
|
|
136
|
+
return v["voice_id"]
|
|
137
|
+
|
|
138
|
+
# Fuzzy match
|
|
139
|
+
for v in voices:
|
|
140
|
+
if voice_name.lower() in v["name"].lower():
|
|
141
|
+
print(f"Matched voice: {v['name']} ({v['voice_id']})", file=sys.stderr)
|
|
142
|
+
return v["voice_id"]
|
|
143
|
+
|
|
144
|
+
print(f"ERROR: Voice '{voice_name}' not found. Run 'tts.py voices' to list available voices.", file=sys.stderr)
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def cmd_speak(args: argparse.Namespace) -> None:
|
|
149
|
+
"""Convert text to speech."""
|
|
150
|
+
text = args.text
|
|
151
|
+
|
|
152
|
+
# Read from stdin if text is "-"
|
|
153
|
+
if text == "-":
|
|
154
|
+
text = sys.stdin.read().strip()
|
|
155
|
+
# Read from file if text starts with @
|
|
156
|
+
elif text.startswith("@"):
|
|
157
|
+
filepath = text[1:]
|
|
158
|
+
text = Path(filepath).read_text(encoding="utf-8").strip()
|
|
159
|
+
|
|
160
|
+
if not text:
|
|
161
|
+
print("ERROR: No text provided", file=sys.stderr)
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
|
|
164
|
+
# Resolve voice
|
|
165
|
+
if args.voice_id:
|
|
166
|
+
voice_id = args.voice_id
|
|
167
|
+
elif args.voice:
|
|
168
|
+
voice_id = resolve_voice_id(args.voice)
|
|
169
|
+
else:
|
|
170
|
+
voice_id = "JBFqnCBsd6RMkjVDRZzb" # Default: George
|
|
171
|
+
|
|
172
|
+
# Build request body
|
|
173
|
+
body: dict = {
|
|
174
|
+
"text": text,
|
|
175
|
+
"model_id": args.model or "eleven_multilingual_v2",
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
voice_settings: dict = {}
|
|
179
|
+
if args.stability is not None:
|
|
180
|
+
voice_settings["stability"] = args.stability
|
|
181
|
+
if args.similarity is not None:
|
|
182
|
+
voice_settings["similarity_boost"] = args.similarity
|
|
183
|
+
if args.style is not None:
|
|
184
|
+
voice_settings["style"] = args.style
|
|
185
|
+
if args.speed is not None:
|
|
186
|
+
voice_settings["speed"] = args.speed
|
|
187
|
+
if voice_settings:
|
|
188
|
+
body["voice_settings"] = voice_settings
|
|
189
|
+
|
|
190
|
+
# Output format
|
|
191
|
+
fmt = args.format or "mp3_44100_128"
|
|
192
|
+
|
|
193
|
+
# Make the request
|
|
194
|
+
audio = api_request("POST", f"/text-to-speech/{voice_id}?output_format={fmt}", body=body, stream=True)
|
|
195
|
+
|
|
196
|
+
# Determine output path
|
|
197
|
+
if args.output:
|
|
198
|
+
out_path = Path(args.output)
|
|
199
|
+
else:
|
|
200
|
+
ext = fmt.split("_")[0] # mp3, pcm, opus, etc
|
|
201
|
+
out_path = Path(f"speech_output.{ext}")
|
|
202
|
+
|
|
203
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
out_path.write_bytes(audio)
|
|
205
|
+
print(f"Audio saved: {out_path} ({len(audio):,} bytes)")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def cmd_batch(args: argparse.Namespace) -> None:
|
|
209
|
+
"""Convert a text file to speech, optionally splitting by paragraphs."""
|
|
210
|
+
input_path = Path(args.input_file)
|
|
211
|
+
if not input_path.exists():
|
|
212
|
+
print(f"ERROR: File not found: {input_path}", file=sys.stderr)
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
|
|
215
|
+
text = input_path.read_text(encoding="utf-8").strip()
|
|
216
|
+
|
|
217
|
+
if args.split_paragraphs:
|
|
218
|
+
chunks = [p.strip() for p in text.split("\n\n") if p.strip()]
|
|
219
|
+
else:
|
|
220
|
+
chunks = [text]
|
|
221
|
+
|
|
222
|
+
# Output directory
|
|
223
|
+
if args.output:
|
|
224
|
+
out_dir = Path(args.output)
|
|
225
|
+
else:
|
|
226
|
+
out_dir = Path(f"{input_path.stem}_audio")
|
|
227
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
228
|
+
|
|
229
|
+
# Resolve voice
|
|
230
|
+
if args.voice_id:
|
|
231
|
+
voice_id = args.voice_id
|
|
232
|
+
elif args.voice:
|
|
233
|
+
voice_id = resolve_voice_id(args.voice)
|
|
234
|
+
else:
|
|
235
|
+
voice_id = "JBFqnCBsd6RMkjVDRZzb"
|
|
236
|
+
|
|
237
|
+
fmt = args.format or "mp3_44100_128"
|
|
238
|
+
ext = fmt.split("_")[0]
|
|
239
|
+
model = args.model or "eleven_multilingual_v2"
|
|
240
|
+
|
|
241
|
+
print(f"Converting {len(chunks)} chunk(s) to speech...")
|
|
242
|
+
for i, chunk in enumerate(chunks, 1):
|
|
243
|
+
body = {"text": chunk, "model_id": model}
|
|
244
|
+
audio = api_request("POST", f"/text-to-speech/{voice_id}?output_format={fmt}", body=body, stream=True)
|
|
245
|
+
out_path = out_dir / f"{i:03d}.{ext}"
|
|
246
|
+
out_path.write_bytes(audio)
|
|
247
|
+
print(f" [{i}/{len(chunks)}] {out_path} ({len(audio):,} bytes) — {chunk[:60]}...")
|
|
248
|
+
|
|
249
|
+
print(f"\nDone. {len(chunks)} audio files saved to: {out_dir}/")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def cmd_clone(args: argparse.Namespace) -> None:
|
|
253
|
+
"""Clone a voice from audio samples."""
|
|
254
|
+
key = get_api_key()
|
|
255
|
+
url = f"{API_BASE}/voices/add"
|
|
256
|
+
|
|
257
|
+
# Build multipart form data manually
|
|
258
|
+
import mimetypes
|
|
259
|
+
boundary = "----KortixBoundary" + os.urandom(8).hex()
|
|
260
|
+
|
|
261
|
+
parts = []
|
|
262
|
+
parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="name"\r\n\r\n{args.name}')
|
|
263
|
+
if args.description:
|
|
264
|
+
parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="description"\r\n\r\n{args.description}')
|
|
265
|
+
|
|
266
|
+
file_parts = []
|
|
267
|
+
for filepath in args.files:
|
|
268
|
+
p = Path(filepath)
|
|
269
|
+
if not p.exists():
|
|
270
|
+
print(f"ERROR: Sample file not found: {p}", file=sys.stderr)
|
|
271
|
+
sys.exit(1)
|
|
272
|
+
mime = mimetypes.guess_type(str(p))[0] or "audio/mpeg"
|
|
273
|
+
file_data = p.read_bytes()
|
|
274
|
+
file_parts.append((p.name, mime, file_data))
|
|
275
|
+
|
|
276
|
+
# Assemble body
|
|
277
|
+
body_bytes = b""
|
|
278
|
+
for part in parts:
|
|
279
|
+
body_bytes += part.encode() + b"\r\n"
|
|
280
|
+
for fname, mime, fdata in file_parts:
|
|
281
|
+
body_bytes += f'--{boundary}\r\nContent-Disposition: form-data; name="files"; filename="{fname}"\r\nContent-Type: {mime}\r\n\r\n'.encode()
|
|
282
|
+
body_bytes += fdata + b"\r\n"
|
|
283
|
+
body_bytes += f"--{boundary}--\r\n".encode()
|
|
284
|
+
|
|
285
|
+
req = urllib.request.Request(url, data=body_bytes, method="POST")
|
|
286
|
+
req.add_header("xi-api-key", key)
|
|
287
|
+
req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
291
|
+
result = json.loads(resp.read().decode())
|
|
292
|
+
print(f"Voice cloned successfully!")
|
|
293
|
+
print(f" Name: {args.name}")
|
|
294
|
+
print(f" Voice ID: {result.get('voice_id', 'unknown')}")
|
|
295
|
+
print(f" Use with: python tts.py speak \"text\" --voice-id {result.get('voice_id', '')}")
|
|
296
|
+
except urllib.error.HTTPError as e:
|
|
297
|
+
error_body = e.read().decode()
|
|
298
|
+
print(f"ERROR [{e.code}]: {error_body}", file=sys.stderr)
|
|
299
|
+
sys.exit(1)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def cmd_sound(args: argparse.Namespace) -> None:
|
|
303
|
+
"""Generate a sound effect from a text prompt."""
|
|
304
|
+
body = {
|
|
305
|
+
"text": args.prompt,
|
|
306
|
+
}
|
|
307
|
+
if args.duration:
|
|
308
|
+
body["duration_seconds"] = args.duration
|
|
309
|
+
|
|
310
|
+
audio = api_request("POST", "/sound-generation", body=body, stream=True)
|
|
311
|
+
|
|
312
|
+
if args.output:
|
|
313
|
+
out_path = Path(args.output)
|
|
314
|
+
else:
|
|
315
|
+
slug = args.prompt[:40].replace(" ", "_").replace("/", "_")
|
|
316
|
+
out_path = Path(f"sfx_{slug}.mp3")
|
|
317
|
+
|
|
318
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
319
|
+
out_path.write_bytes(audio)
|
|
320
|
+
print(f"Sound effect saved: {out_path} ({len(audio):,} bytes)")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ── CLI Parser ───────────────────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def main() -> None:
|
|
327
|
+
parser = argparse.ArgumentParser(
|
|
328
|
+
prog="tts",
|
|
329
|
+
description="Kortix TTS — ElevenLabs Text-to-Speech CLI",
|
|
330
|
+
)
|
|
331
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
332
|
+
|
|
333
|
+
# speak
|
|
334
|
+
p_speak = sub.add_parser("speak", help="Convert text to speech")
|
|
335
|
+
p_speak.add_argument("text", help="Text to speak (use '-' for stdin, '@file' to read from file)")
|
|
336
|
+
p_speak.add_argument("-o", "--output", help="Output file path")
|
|
337
|
+
p_speak.add_argument("--voice", help="Voice name (e.g. 'Rachel', 'George')")
|
|
338
|
+
p_speak.add_argument("--voice-id", help="Voice ID directly")
|
|
339
|
+
p_speak.add_argument("--model", help="Model ID (default: eleven_multilingual_v2)")
|
|
340
|
+
p_speak.add_argument("--format", help="Output format (default: mp3_44100_128)")
|
|
341
|
+
p_speak.add_argument("--stability", type=float, help="Voice stability 0.0-1.0")
|
|
342
|
+
p_speak.add_argument("--similarity", type=float, help="Similarity boost 0.0-1.0")
|
|
343
|
+
p_speak.add_argument("--style", type=float, help="Style exaggeration 0.0-1.0")
|
|
344
|
+
p_speak.add_argument("--speed", type=float, help="Speed 0.5-2.0")
|
|
345
|
+
|
|
346
|
+
# voices
|
|
347
|
+
p_voices = sub.add_parser("voices", help="List available voices")
|
|
348
|
+
p_voices.add_argument("--search", help="Search/filter voices")
|
|
349
|
+
|
|
350
|
+
# models
|
|
351
|
+
sub.add_parser("models", help="List available TTS models")
|
|
352
|
+
|
|
353
|
+
# clone
|
|
354
|
+
p_clone = sub.add_parser("clone", help="Clone a voice from audio samples")
|
|
355
|
+
p_clone.add_argument("name", help="Name for the cloned voice")
|
|
356
|
+
p_clone.add_argument("files", nargs="+", help="Audio sample files (mp3, wav, etc.)")
|
|
357
|
+
p_clone.add_argument("--description", help="Voice description")
|
|
358
|
+
|
|
359
|
+
# batch
|
|
360
|
+
p_batch = sub.add_parser("batch", help="Convert text file to speech")
|
|
361
|
+
p_batch.add_argument("input_file", help="Input text file")
|
|
362
|
+
p_batch.add_argument("-o", "--output", help="Output directory")
|
|
363
|
+
p_batch.add_argument("--voice", help="Voice name")
|
|
364
|
+
p_batch.add_argument("--voice-id", help="Voice ID directly")
|
|
365
|
+
p_batch.add_argument("--model", help="Model ID")
|
|
366
|
+
p_batch.add_argument("--format", help="Output format")
|
|
367
|
+
p_batch.add_argument("--split-paragraphs", action="store_true", help="Split by paragraphs into separate files")
|
|
368
|
+
|
|
369
|
+
# sound
|
|
370
|
+
p_sound = sub.add_parser("sound", help="Generate sound effect from text prompt")
|
|
371
|
+
p_sound.add_argument("prompt", help="Description of the sound effect")
|
|
372
|
+
p_sound.add_argument("-o", "--output", help="Output file path")
|
|
373
|
+
p_sound.add_argument("--duration", type=float, help="Duration in seconds")
|
|
374
|
+
|
|
375
|
+
args = parser.parse_args()
|
|
376
|
+
|
|
377
|
+
commands = {
|
|
378
|
+
"speak": cmd_speak,
|
|
379
|
+
"voices": cmd_voices,
|
|
380
|
+
"models": cmd_models,
|
|
381
|
+
"clone": cmd_clone,
|
|
382
|
+
"batch": cmd_batch,
|
|
383
|
+
"sound": cmd_sound,
|
|
384
|
+
}
|
|
385
|
+
commands[args.command](args)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
if __name__ == "__main__":
|
|
389
|
+
main()
|