Skip to content

Commit 75bf509

Browse files
committed
Small fixes
1 parent d85cbab commit 75bf509

2 files changed

Lines changed: 128 additions & 2 deletions

File tree

cmd/tools/tools.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@ package tools
33
import (
44
"fmt"
55
"github.com/spf13/cobra"
6+
"github.com/warmans/tvgif/pkg/model"
7+
"github.com/warmans/tvgif/pkg/srt"
68
"github.com/warmans/tvgif/pkg/util"
79
"io"
810
"log/slog"
11+
"math"
912
"os"
1013
"regexp"
1114
"strings"
15+
"time"
16+
"unicode"
1217
)
1318

1419
var (
@@ -25,10 +30,131 @@ func NewToolsCommand(logger *slog.Logger) *cobra.Command {
2530
cmd.Flags().StringVar(&metadataPath, "o", "./var/metadata", "output metadata to this path")
2631

2732
cmd.AddCommand(NewFixNameCommand())
33+
cmd.AddCommand(NewRepairSRTCommand())
2834

2935
return cmd
3036
}
3137

38+
func NewRepairSRTCommand() *cobra.Command {
39+
cmd := &cobra.Command{
40+
Use: "repair-srt [file]",
41+
Short: "repair an .srt file in place (e.g. fix incorrect text case such as all upper-case dialog)",
42+
Args: cobra.ExactArgs(1),
43+
RunE: func(cmd *cobra.Command, args []string) error {
44+
fileName := args[0]
45+
46+
f, err := os.Open(fileName)
47+
if err != nil {
48+
return fmt.Errorf("failed to open file '%s': %w", fileName, err)
49+
}
50+
51+
// Parse with no duration limit and without eliminating gaps so that
52+
// the original timestamps are preserved as closely as possible.
53+
dialog, err := srt.Read(f, false, time.Duration(math.MaxInt64))
54+
if err != nil {
55+
_ = f.Close()
56+
return fmt.Errorf("failed to parse srt: %w", err)
57+
}
58+
if err := f.Close(); err != nil {
59+
return err
60+
}
61+
62+
for i := range dialog {
63+
dialog[i].Content = fixCase(dialog[i].Content)
64+
}
65+
66+
if err := os.WriteFile(fileName, []byte(renderSRT(dialog)), 0644); err != nil {
67+
return fmt.Errorf("failed to write repaired srt: %w", err)
68+
}
69+
70+
_, err = fmt.Fprintf(os.Stdout, "repaired %d lines of dialog in %s\n", len(dialog), fileName)
71+
return err
72+
},
73+
}
74+
75+
return cmd
76+
}
77+
78+
// renderSRT serializes dialog back into the SRT file format.
79+
func renderSRT(dialog []model.Dialog) string {
80+
sb := &strings.Builder{}
81+
for _, d := range dialog {
82+
fmt.Fprintf(sb, "%d\n", d.Pos)
83+
fmt.Fprintf(sb, "%s --> %s\n", formatTimestamp(d.StartTimestamp), formatTimestamp(d.EndTimestamp))
84+
sb.WriteString(d.Content)
85+
sb.WriteString("\n\n")
86+
}
87+
return sb.String()
88+
}
89+
90+
// formatTimestamp renders a duration as an SRT timestamp (HH:MM:SS,mmm).
91+
func formatTimestamp(d time.Duration) string {
92+
hours := d / time.Hour
93+
d -= hours * time.Hour
94+
minutes := d / time.Minute
95+
d -= minutes * time.Minute
96+
seconds := d / time.Second
97+
d -= seconds * time.Second
98+
millis := d / time.Millisecond
99+
return fmt.Sprintf("%02d:%02d:%02d,%03d", hours, minutes, seconds, millis)
100+
}
101+
102+
var standaloneI = regexp.MustCompile(`\bi(?:('[a-z]+)|\b)`)
103+
104+
// fixCase corrects the case of a line of dialog. If the text is "shouty"
105+
// (i.e. contains letters but none of them are lower-case) it is converted
106+
// to sentence case. Otherwise it is left untouched.
107+
func fixCase(s string) string {
108+
if !isShouty(s) {
109+
return s
110+
}
111+
return toSentenceCase(s)
112+
}
113+
114+
// isShouty reports whether a string is predominantly upper-case, allowing for
115+
// a few stray lower-case letters. A minimum number of letters is required to
116+
// avoid mis-classifying very short strings (e.g. a lone "A" or "I").
117+
func isShouty(s string) bool {
118+
var upper, lower int
119+
for _, r := range s {
120+
switch {
121+
case unicode.IsUpper(r):
122+
upper++
123+
case unicode.IsLower(r):
124+
lower++
125+
}
126+
}
127+
total := upper + lower
128+
if total < 3 {
129+
return false
130+
}
131+
return float64(upper)/float64(total) >= 0.6
132+
}
133+
134+
// toSentenceCase lower-cases the input and then capitalizes the first letter
135+
// of each sentence, as well as the standalone pronoun "i".
136+
func toSentenceCase(s string) string {
137+
runes := []rune(strings.ToLower(s))
138+
capitalizeNext := true
139+
for i, r := range runes {
140+
if unicode.IsLetter(r) {
141+
if capitalizeNext {
142+
runes[i] = unicode.ToUpper(r)
143+
capitalizeNext = false
144+
}
145+
continue
146+
}
147+
switch r {
148+
case '.', '!', '?', '\n':
149+
capitalizeNext = true
150+
}
151+
}
152+
result := string(runes)
153+
return standaloneI.ReplaceAllStringFunc(result, func(match string) string {
154+
return "I" + match[1:]
155+
})
156+
}
157+
32158
func NewFixNameCommand() *cobra.Command {
33159
cmd := &cobra.Command{
34160
Use: "fix-name",

pkg/discord/bot.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ func (b *Bot) updatePreview(s *discordgo.Session, i *discordgo.InteractionCreate
523523
return
524524
}
525525
go func() {
526-
interactionResponse, err = b.buildInteractionResponseForPreview(
526+
interactionResponse, err := b.buildInteractionResponseForPreview(
527527
dialogWithContext,
528528
sta,
529529
responseWithUsername(username),
@@ -606,7 +606,7 @@ func (b *Bot) createPreview(
606606

607607
// update with the gif
608608
go func() {
609-
interactionResponse, err = b.buildInteractionResponseForPreview(
609+
interactionResponse, err := b.buildInteractionResponseForPreview(
610610
dialogWithContext,
611611
state,
612612
responseWithUsername(username),

0 commit comments

Comments
 (0)