init commit
This commit is contained in:
112
internal/srt/parser.go
Normal file
112
internal/srt/parser.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package srt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
astisub "github.com/asticode/go-astisub"
|
||||
)
|
||||
|
||||
// Load loads an SRT file from the given path
|
||||
func Load(path string) (*Transcript, error) {
|
||||
subs, err := astisub.OpenFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse SRT file: %w", err)
|
||||
}
|
||||
|
||||
transcript := &Transcript{
|
||||
FilePath: path,
|
||||
IsTemp: strings.HasSuffix(path, ".tmp"),
|
||||
Cues: make([]Cue, len(subs.Items)),
|
||||
}
|
||||
|
||||
lineNum := 1 // SRT files are 1-indexed
|
||||
for i, item := range subs.Items {
|
||||
var textParts []string
|
||||
for _, line := range item.Lines {
|
||||
var lineParts []string
|
||||
for _, lineItem := range line.Items {
|
||||
lineParts = append(lineParts, lineItem.Text)
|
||||
}
|
||||
textParts = append(textParts, strings.Join(lineParts, ""))
|
||||
}
|
||||
|
||||
text := strings.Join(textParts, "\n")
|
||||
transcript.Cues[i] = Cue{
|
||||
Index: i + 1,
|
||||
Start: item.StartAt,
|
||||
End: item.EndAt,
|
||||
Text: text,
|
||||
LineNumber: lineNum,
|
||||
}
|
||||
|
||||
// Calculate lines used by this cue:
|
||||
// 1 (index) + 1 (timestamp) + text lines + 1 (blank line)
|
||||
textLines := 1
|
||||
if text != "" {
|
||||
textLines = strings.Count(text, "\n") + 1
|
||||
}
|
||||
lineNum += 2 + textLines + 1 // index + timestamp + text + blank
|
||||
}
|
||||
|
||||
return transcript, nil
|
||||
}
|
||||
|
||||
// FindTranscript looks for an SRT file next to the audio file
|
||||
func FindTranscript(audioPath string) string {
|
||||
ext := filepath.Ext(audioPath)
|
||||
basePath := strings.TrimSuffix(audioPath, ext)
|
||||
|
||||
// Try common SRT naming patterns
|
||||
patterns := []string{
|
||||
basePath + ".srt",
|
||||
basePath + ".en.srt",
|
||||
audioPath + ".srt",
|
||||
}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
if _, err := os.Stat(pattern); err == nil {
|
||||
return pattern
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// CreateTempTranscript creates a temporary SRT file with placeholder content
|
||||
func CreateTempTranscript(audioPath string) (string, error) {
|
||||
basename := filepath.Base(audioPath)
|
||||
ext := filepath.Ext(basename)
|
||||
nameOnly := strings.TrimSuffix(basename, ext)
|
||||
|
||||
tempPath := filepath.Join(os.TempDir(), nameOnly+".srt.tmp")
|
||||
|
||||
content := fmt.Sprintf(`1
|
||||
00:00:00,000 --> 00:00:05,000
|
||||
[No transcript found for: %s]
|
||||
|
||||
2
|
||||
00:00:05,000 --> 00:00:15,000
|
||||
This is a temporary transcript file.
|
||||
You can edit it using vim-style commands.
|
||||
Press 'i' to enter edit mode, 'esc' to exit.
|
||||
|
||||
3
|
||||
00:00:15,000 --> 00:00:25,000
|
||||
To generate a transcript automatically, try:
|
||||
https://git.beitzah.net/ysandler/transcribe
|
||||
|
||||
4
|
||||
00:00:25,000 --> 00:00:35,000
|
||||
Or launch with an existing transcript:
|
||||
playback %s -t /path/to/transcript.srt
|
||||
`, basename, basename)
|
||||
|
||||
if err := os.WriteFile(tempPath, []byte(content), 0644); err != nil {
|
||||
return "", fmt.Errorf("failed to create temp transcript: %w", err)
|
||||
}
|
||||
|
||||
return tempPath, nil
|
||||
}
|
||||
39
internal/srt/types.go
Normal file
39
internal/srt/types.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package srt
|
||||
|
||||
import "time"
|
||||
|
||||
// Cue represents a single subtitle entry
|
||||
type Cue struct {
|
||||
Index int
|
||||
Start time.Duration
|
||||
End time.Duration
|
||||
Text string
|
||||
LineNumber int // Line number in the SRT file (1-indexed)
|
||||
}
|
||||
|
||||
// Transcript represents a complete subtitle file
|
||||
type Transcript struct {
|
||||
Cues []Cue
|
||||
FilePath string
|
||||
IsTemp bool
|
||||
}
|
||||
|
||||
// CueAt returns the cue that contains the given time position
|
||||
func (t *Transcript) CueAt(pos time.Duration) *Cue {
|
||||
for i := range t.Cues {
|
||||
if pos >= t.Cues[i].Start && pos < t.Cues[i].End {
|
||||
return &t.Cues[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CueIndexAt returns the index of the cue at the given position, or -1
|
||||
func (t *Transcript) CueIndexAt(pos time.Duration) int {
|
||||
for i := range t.Cues {
|
||||
if pos >= t.Cues[i].Start && pos < t.Cues[i].End {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
79
internal/srt/writer.go
Normal file
79
internal/srt/writer.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package srt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// formatDuration formats a duration as SRT timestamp (HH:MM:SS,mmm)
|
||||
func formatDuration(d int64) string {
|
||||
ms := d % 1000
|
||||
d /= 1000
|
||||
s := d % 60
|
||||
d /= 60
|
||||
m := d % 60
|
||||
h := d / 60
|
||||
|
||||
return fmt.Sprintf("%02d:%02d:%02d,%03d", h, m, s, ms)
|
||||
}
|
||||
|
||||
// Save writes the transcript to an SRT file
|
||||
func (t *Transcript) Save() error {
|
||||
return t.SaveTo(t.FilePath)
|
||||
}
|
||||
|
||||
// SaveTo writes the transcript to the specified path
|
||||
func (t *Transcript) SaveTo(path string) error {
|
||||
var sb strings.Builder
|
||||
|
||||
for i, cue := range t.Cues {
|
||||
if i > 0 {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%d\n", cue.Index))
|
||||
sb.WriteString(fmt.Sprintf("%s --> %s\n",
|
||||
formatDuration(cue.Start.Milliseconds()),
|
||||
formatDuration(cue.End.Milliseconds())))
|
||||
sb.WriteString(cue.Text)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, []byte(sb.String()), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PromoteTempFile saves the transcript to a permanent location
|
||||
func (t *Transcript) PromoteTempFile(audioPath string) (string, error) {
|
||||
if !t.IsTemp {
|
||||
return t.FilePath, nil
|
||||
}
|
||||
|
||||
// Create permanent path next to audio file
|
||||
ext := filepath.Ext(audioPath)
|
||||
permanentPath := strings.TrimSuffix(audioPath, ext) + ".srt"
|
||||
|
||||
if err := t.SaveTo(permanentPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Update transcript state
|
||||
t.FilePath = permanentPath
|
||||
t.IsTemp = false
|
||||
|
||||
// Remove temp file
|
||||
tempPath := filepath.Join(os.TempDir(), filepath.Base(audioPath))
|
||||
os.Remove(strings.TrimSuffix(tempPath, filepath.Ext(tempPath)) + ".srt.tmp")
|
||||
|
||||
return permanentPath, nil
|
||||
}
|
||||
Reference in New Issue
Block a user