A Go library and CLI tool for generating ASS (Advanced SubStation Alpha) subtitle files with word-by-word fade-in effects. Perfect for creating dynamic subtitles where words appear sequentially with smooth fade transitions.
- Word-by-word fade effects: Each word fades in sequentially using ASS
\alphatags - Structured JSON input: Generate from timed words (
word,start,endin seconds) with--json/--json-file;StartDelayshifts all timings (e.g. 1500 ms = 1.5 s offset) - Karaoke support: Optional karaoke mode with
\ktags for word highlighting - Shadow effects: Customizable shadow with directional control using
\xshadand\yshadtags - Flexible configuration: Customizable fonts, colors, timing, alignment, and styling
- Library API: Use as a Go library in your own projects
- CLI tool: Easy-to-use command-line interface with structured logging
Here's a visual example of the word-by-word fade effect generated by wordsubgen:
This demo was generated using the following command:
wordsubgen --lines "This is a super test|Hello world \!" --font "Outfit" --fontsize 85 --shadow-enabled --shadow-x 3 --shadow-y 8 --width 1080 --height 734 --out subtitle.assTo install the command-line tool:
go install github.com/kperreau/wordsubgen/cmd/wordsubgen@latestTo use wordsubgen as a library in your Go project:
go get github.com/kperreau/wordsubgen@latestThen import it in your Go code:
import "github.com/kperreau/wordsubgen"For development work on the wordsubgen project itself:
git clone https://github.com/kperreau/wordsubgen.git
cd wordsubgen
make deps
make buildOr install the CLI to your GOPATH/bin:
make install # Installs to GOPATH/bin- Go 1.25.0+: Required for building from source
- Make: Required for using the Makefile (optional, you can use
go builddirectly)
Generate subtitles from command line:
# Basic usage
wordsubgen --lines "Hello world|This is a test" --out subtitle.ass
# Custom styling
wordsubgen --lines "Hello world" --color "#FF0000" --fontsize 72 --delay 500
# Karaoke mode
wordsubgen --lines "Hello world" --karaoke --delay 400
# From file
wordsubgen --file input.txt --out subtitle.ass
# From structured JSON (words with start/end in seconds); --start-delay 1500 = 1.5 s shift
wordsubgen --json-file words.json --start-delay 1500 --out subtitle.ass
# Show help with current default values
wordsubgen --helppackage main
import (
"log"
"github.com/kperreau/wordsubgen"
)
func main() {
// Create configuration
cfg := wordsubgen.DefaultConfig()
cfg.FontSize = 48
cfg.PrimaryColor = wordsubgen.ColorToASS("#FF0000")
cfg.PerWordDelay = 500
// Enable shadow with diagonal direction (more downward than sideways)
cfg.ShadowEnabled = true
cfg.ShadowX = 3 // Horizontal offset
cfg.ShadowY = 8 // Vertical offset (more downward)
// Generate ASS content
lines := []string{"Hello world", "This is a test"}
content, err := wordsubgen.GenerateASS(cfg, lines)
if err != nil {
log.Fatal(err)
}
// Write to file
err = wordsubgen.WriteASS("output.ass", content, cfg.Logger)
if err != nil {
log.Fatal(err)
}
}You can generate subtitles from a JSON structure with a segments array. Each segment is a phrase with a words array; each word has word, start and end (in seconds). Extra fields anywhere are ignored.
{
"segments": [
{
"words": [
{"word": "Découvrez", "start": 0.162, "end": 1.024},
{"word": "cette", "start": 1.064, "end": 1.344},
{"word": "superbe", "start": 1.404, "end": 1.845}
]
},
{
"words": [
{"word": "Deuxième", "start": 2.0, "end": 2.5},
{"word": "phrase", "start": 2.6, "end": 3.0}
]
}
]
}- StartDelay (e.g.
--start-delay 1500): adds 1.5 seconds to everystartandendso subtitles start 1.5 s later in the video. - PerWordDelay is ignored (timings come from the JSON).
- FadeDuration is still used for the word fade-in effect.
wordsubgen --json-file words.json --start-delay 1500 --out subtitle.ass
wordsubgen --json '{"segments":[{"words":[{"word":"Hello","start":0,"end":0.5},{"word":"world","start":0.6,"end":1.2}]}]}' --out subtitle.assdata, _ := os.ReadFile("words.json")
phrases, err := wordsubgen.ParseStructuredJSON(data)
if err != nil {
log.Fatal(err)
}
cfg := wordsubgen.DefaultConfig()
cfg.StartDelay = 1500 // 1.5 s shift
content, err := wordsubgen.GenerateASSFromStructured(cfg, phrases)
// then wordsubgen.WriteASS(...)wordsubgen features a flexible logging interface that allows you to inject your own logger implementation. The library comes with several built-in loggers and supports popular logging libraries like zerolog (recommended), logrus, and others.
// Default logger (uses Go's standard log package)
cfg.Logger = wordsubgen.NewDefaultLogger()
// No-operation logger (silent, useful for tests)
cfg.Logger = wordsubgen.NewNoOpLogger()type MyCustomLogger struct{}
func (l *MyCustomLogger) Debug(msg string, fields ...wordsubgen.Field) {
// Your debug implementation
}
func (l *MyCustomLogger) Info(msg string, fields ...wordsubgen.Field) {
// Your info implementation
}
func (l *MyCustomLogger) Error(msg string, fields ...wordsubgen.Field) {
// Your error implementation
}
func (l *MyCustomLogger) Warn(msg string, fields ...wordsubgen.Field) {
// Your warning implementation
}
// Use your custom logger
cfg.Logger = &MyCustomLogger{}- Structured logging: Support for key-value fields
- Multiple log levels: Debug, Info, Error, Warn
- Zero dependencies: No forced logging library dependencies
- Flexible: Easy to integrate with any logging system
- zerolog support: Recommended for high-performance structured logging
| Option | Type | Default | Description |
|---|---|---|---|
Width |
int | 1080 | Video width in pixels |
Height |
int | 1920 | Video height in pixels |
| Option | Type | Default | Description |
|---|---|---|---|
FontName |
string | "Arial" | Font family name |
FontSize |
int | 64 | Font size in points |
| Option | Type | Default | Description |
|---|---|---|---|
PrimaryColor |
string | "&H00FFFFFF" | Text color (white) |
SecondaryColor |
string | "&H0000FFFF" | Karaoke highlight color (yellow) |
OutlineColor |
string | "&H00000000" | Outline color (black) |
BackColor |
string | "&H64000000" | Background color (semi-transparent) |
Color Conversion: Use
ColorToASS()to convert hex colors (#RRGGBB) to ASS format, andColorToHex()to convert ASS colors back to hex format.
| Option | Type | Default | Description |
|---|---|---|---|
Bold |
bool | true | Bold text |
Italic |
bool | false | Italic text |
Underline |
bool | false | Underlined text |
StrikeOut |
bool | false | Strikethrough text |
| Option | Type | Default | Description |
|---|---|---|---|
ScaleX |
int | 100 | Horizontal scale (%) |
ScaleY |
int | 100 | Vertical scale (%) |
Spacing |
int | 0 | Character spacing |
Angle |
int | 0 | Text rotation angle |
| Option | Type | Default | Description |
|---|---|---|---|
BorderStyle |
int | 1 | Border style (1=outline, 3=box) |
Outline |
int | 4 | Outline width |
Shadow |
int | 0 | Shadow width |
| Option | Type | Default | Description |
|---|---|---|---|
ShadowEnabled |
bool | false | Enable/disable shadow effect |
ShadowX |
int | 3 | Horizontal shadow offset (-50 to 50) |
ShadowY |
int | 8 | Vertical shadow offset (-50 to 50) |
Shadow Direction: Positive values move the shadow right (X) and down (Y). Negative values move left and up. The default values (X=3, Y=8) create a diagonal shadow that's more downward than sideways.
| Option | Type | Default | Description |
|---|---|---|---|
Alignment |
int | 2 | Text alignment (1=bottom-left, 2=bottom-center, 3=bottom-right, etc.) |
| Option | Type | Default | Description |
|---|---|---|---|
MarginL |
int | 40 | Left margin |
MarginR |
int | 40 | Right margin |
MarginV |
int | 120 | Vertical margin |
| Option | Type | Default | Description |
|---|---|---|---|
StartDelay |
int | 0 | Delay before starting subtitles (milliseconds). In structured JSON mode: added to all word start/end times (e.g. 1500 = 1.5 s shift). |
PerWordDelay |
int | 300 | Delay between words (milliseconds). Ignored when using structured JSON (GenerateASSFromStructured). |
FadeDuration |
int | 140 | Fade duration (milliseconds) |
LineHold |
int | 2000 | Line hold duration (milliseconds). Not applied in structured JSON mode. |
LineGap |
int | 0 | Gap between lines (milliseconds). Not applied in structured JSON mode. |
| Option | Type | Default | Description |
|---|---|---|---|
Karaoke |
bool | false | Enable karaoke mode |
| Option | Type | Default | Description |
|---|---|---|---|
Encoding |
int | 1 | Text encoding (1=ANSI, 0=Unicode) |
Note: All default values are dynamically generated from the
DefaultConfig()function. Use--helpto see the current default values.
--lines string: Input lines separated by|(e.g., 'Hello world|Second line')--file string: Input file with lines (one per line)--json string: Structured JSON:{"segments":[{"words":[{"word":"...","start":0.1,"end":0.5},...]},...]}.start/endin seconds. Extra fields ignored.--json-file string: Path to a structured JSON file (same format as--json).
With --json or --json-file: --delay (PerWordDelay) is ignored; --start-delay shifts all word timings (e.g. --start-delay 1500 adds 1.5 s to every start/end).
--out string: Output ASS file path (default: output.ass)
--width int: Video width (default: 1080)--height int: Video height (default: 1920)
--font string: Font name (default: Arial)--fontsize int: Font size (default: 64)
--color string: Primary text color (default: #FFFFFF)--secondary string: Secondary color for karaoke (default: #FFFF00)--outline string: Outline color (default: #000000)--background string: Background color in ASS format (default: #64000000)
--bold: Bold text (default: true)--italic: Italic text (default: false)--underline: Underline text (default: false)--strikeout: Strikeout text (default: false)
--scalex int: Horizontal scale % (default: 100)--scaley int: Vertical scale % (default: 100)--spacing int: Character spacing (default: 0)--angle int: Text angle (default: 0)
--borderstyle int: Border style (default: 1)--outlinewidth int: Outline width (default: 4)--shadow int: Shadow width (default: 0)
--shadow-enabled: Enable shadow effect (default: false)--shadow-x int: Horizontal shadow offset (default: 3)--shadow-y int: Vertical shadow offset (default: 8)
--alignment int: Text alignment (1=bottom-left, 2=bottom-center, 3=bottom-right, etc.) (default: 2)
--marginl int: Left margin (default: 40)--marginr int: Right margin (default: 40)--marginv int: Vertical margin (default: 120)
--start-delay int: Delay before starting subtitles in ms (default: 0)--delay int: Delay between words in ms (default: 300)--fade int: Fade duration in ms (default: 140)--hold int: Line hold duration in ms (default: 2000)--gap int: Gap between lines in ms (default: 0)
--karaoke: Enable karaoke mode (default: false)
--help: Show help with current default values
wordsubgen --lines "Hello world|This is a test" --out subtitle.asswordsubgen --lines "Hello world" --fontsize 48 --color "#FF0000" --boldwordsubgen --lines "Hello world" --karaoke --delay 500echo -e "First line\nSecond line\nThird line" > input.txt
wordsubgen --file input.txt --out subtitle.ass# Diagonal shadow (more downward than sideways)
wordsubgen --lines "Hello world" --shadow-enabled --shadow-x 3 --shadow-y 8
# Horizontal shadow (more sideways)
wordsubgen --lines "Hello world" --shadow-enabled --shadow-x 8 --shadow-y 3
# No shadow (explicitly disabled)
wordsubgen --lines "Hello world" --shadow-enabled=false# Delay subtitles by 100ms
wordsubgen --lines "Hello world" --start-delay 100
# Delay subtitles by 1 second
wordsubgen --lines "Hello world" --start-delay 1000
# Delay subtitles by 2.5 seconds
wordsubgen --lines "Hello world" --start-delay 2500# From file with 1.5 s global delay
wordsubgen --json-file words.json --start-delay 1500 --out subtitle.ass
# Inline JSON (one phrase)
wordsubgen --json '{"segments":[{"words":[{"word":"Hello","start":0,"end":0.5},{"word":"world","start":0.6,"end":1.2}]}]}' --out subtitle.ass# Generate sample files
make run-cli # Basic sample
make run-cli-custom # Custom styling
make run-cli-karaoke # Karaoke modewordsubgen/
├── cmd/wordsubgen/
│ ├── main.go # CLI application entry point
│ └── wordsubgen # Built binary (after make build)
├── assets/ # Assets
│ └── example.gif # Demo animation
├── config.go # Configuration and validation
├── config_test.go # Configuration tests
├── generator.go # ASS generation logic
├── generator_test.go # Generator tests
├── integration_test.go # Integration tests
├── logger.go # Flexible logging interface
├── logger_test.go # Logger tests
├── writer.go # File I/O operations
├── writer_test.go # Writer tests
├── go.mod # Go module definition
├── go.sum # Go module checksums
├── Makefile # Build automation
├── README.md # Documentation
├── LICENSE # MIT License
├── input.txt # Sample input file
├── subs.exemple.ass # Example subtitle file
└── subtitle.ass # Generated subtitle file
make build # Build CLI binary
make install # Install CLI to GOPATH/bin
make clean # Clean build artifactsmake test # Run all tests
make test-coverage # Run tests with coverage report
make check # Run fmt, lint, and testmake run-cli # Run CLI with sample input
make run-cli-custom # Run CLI with custom styling
make run-cli-karaoke # Run CLI with karaoke modemake fmt # Format code
make lint # Lint code
make deps # Install dependenciesmake dev # Complete dev workflow: deps, fmt, lint, test, build
make release # Build release binaries for multiple platformsmake help # Show all available targetsThe project includes test coverage:
- Unit tests: Individual component testing (
*_test.gofiles)
Run tests with:
make test # Run all tests
make test-coverage # Generate coverage report
make ci-test # Run all tests for cipackage main
import (
"log"
"github.com/kperreau/wordsubgen"
)
func main() {
// Create configuration with custom logger
cfg := wordsubgen.DefaultConfig()
cfg.Logger = wordsubgen.NewDefaultLogger() // or your custom logger
// Generate subtitles
lines := []string{"Hello world", "This is a test"}
content, err := wordsubgen.GenerateASS(cfg, lines)
if err != nil {
log.Fatal(err)
}
// Write to file
err = wordsubgen.WriteASS("output.ass", content, cfg.Logger)
if err != nil {
log.Fatal(err)
}
}# Show help with current default values
wordsubgen --helpThis will display all available options with their current default values, which are dynamically generated from the DefaultConfig() function.
The generated ASS files follow the Advanced SubStation Alpha v4.00+ format with:
- Script Info: Basic metadata and video resolution
- V4+ Styles: Font, color, and styling definitions
- Events: Dialogue lines with timing and effects
Each word uses the \alpha tag for fade effects:
{\alpha&HFF&\t(start,end,\alpha&H00&)}word
Where:
\alpha&HFF&: Start invisible (fully transparent)\t(start,end,\alpha&H00&): Transition to visible over timestart,end: Timing in milliseconds
When karaoke mode is enabled, words also include \k tags:
{\k100}word
Where 100 is the duration in centiseconds.
When shadow is enabled, text includes \xshad and \yshad tags:
{\xshad3\yshad8}word
Where:
\xshad3: Horizontal shadow offset of 3 pixels\yshad8: Vertical shadow offset of 8 pixels
This creates a diagonal shadow that's more downward than sideways, as requested in the original specification.
- 🎯 Simple Structure: Clean, flat project structure with all source files at the root
- 🔌 Flexible Logging: Injectable logger interface supporting multiple logging libraries
- ⚡ Zero Dependencies: Core library has no forced external dependencies
- 📝 Structured Logging: Built-in support for structured logging with key-value fields
- 🛠️ Easy Integration: Simple API that works with any Go project
- 🎨 Rich Configuration: Extensive customization options for fonts, colors, timing, and styling
- 📊 Performance: Optimized for performance with zero-allocation logging options
- 🔧 Developer Friendly: Comprehensive CLI with verbose logging and helpful error messages
- 🔄 Consistent Defaults: CLI flags automatically use configuration defaults, ensuring consistency
- 🎨 Color Conversion: Built-in functions to convert between hex and ASS color formats
- 🌍 Encoding Support: Built-in support for different text encodings (ANSI/Unicode)
This project is licensed under the MIT License - see the LICENSE file for details.
We welcome contributions! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feat/amazing-feature) - Make your changes
- Add tests if applicable
- Run the test suite (
make test) - Commit your changes (
git commit -m 'feat: add some amazing feature') - Push to the branch (
git push origin feat/amazing-feature) - Open a Pull Request
- Follow Go best practices and conventions
- Add tests for new functionality
- Update documentation as needed
- Use the provided Makefile targets for consistency
- Ensure all tests pass before submitting