Skip to content

New Plugin c/o ChatGPT #262

@fointypinger

Description

@fointypinger

I thought I'd see if I could get ChatGPT to help write a plugin for Subtitle Edit that would allow me to select all highlighted problem lines for targeted correction (a feature I've wanted incorporating into SE for a long time). This would allow, for example, the auto-balancing of only lines deemed to be of excessive length (rather than every single line, as is currently the case).

Unfortunately, I kept hitting a snag seemingly relating to Nikse.SubtitleEdit.PluginInterface.dll (in that the code below would build successfully in Visual Studio but not appear in the SE Plugins section) and ChatGPT could offer no useful advice about how to resolve this issue.

Could anyone who knows anything about Subtitle Edit plugins please take a look at the code below and let me know if it was even anywhere close to working? Possibly this whole endeavour was a waste of time, given that I know nothing about C#, but it seemed worth a try!

using Nikse.SubtitleEdit.PluginInterface;
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Text;
using System.Text.RegularExpressions;

namespace SubtitleEdit.ProblemLineHighlighter
{
    public class ProblemLinePlugin : IPlugin
    {
        private Subtitle _subtitle;
        private SubRip _subRipFormat;

        public string Name => "Problem Line Highlighter";
        public string Text => "Highlight problem lines in subtitles";
        public string Version => "1.0";

        public void Initialize()
        {
            _subRipFormat = new SubRip();
            _subtitle = new Subtitle(_subRipFormat);
        }

        public bool DoesShowDialog()
        {
            return true;
        }

        public void ShowDialog()
        {
            MessageBox.Show("Problem Line Highlighter Plugin loaded!");
            // Here you can later add UI or further plugin logic
        }

        // You can expose your subtitle loading logic here, example:
        public void LoadSubtitleFromLines(string[] lines)
        {
            _subtitle.Paragraphs.Clear();
            var listLines = new List<string>(lines);
            _subRipFormat.LoadSubtitle(_subtitle, listLines, "Example.srt");
            _subtitle.Renumber(1);
        }

        public string GetSubtitleText()
        {
            return _subtitle.ToText();
        }
    }

    // Paste your Subtitle, SubRip, Paragraph, TimeCode classes here (internal or public as needed)
    // For brevity, you can keep them as internal classes below the plugin class,
    // or move to separate files as your project grows.

    internal class Paragraph
    {
        public int Number { get; set; }
        public TimeCode StartTime { get; set; }
        public TimeCode EndTime { get; set; }
        public string Text { get; set; }

        public Paragraph()
        {
            Text = string.Empty;
            StartTime = new TimeCode();
            EndTime = new TimeCode();
        }
    }

    internal class TimeCode
    {
        public int Hours { get; set; }
        public int Minutes { get; set; }
        public int Seconds { get; set; }
        public int Milliseconds { get; set; }

        public TimeCode() { }

        public TimeCode(int hours, int minutes, int seconds, int milliseconds)
        {
            Hours = hours;
            Minutes = minutes;
            Seconds = seconds;
            Milliseconds = milliseconds;
        }

        public override string ToString()
        {
            return $"{Hours:D2}:{Minutes:D2}:{Seconds:D2},{Milliseconds:D3}";
        }

        public void AddTime(TimeSpan timeSpan)
        {
            var totalMs = ToTimeSpan().TotalMilliseconds + timeSpan.TotalMilliseconds;
            if (totalMs < 0) totalMs = 0;

            var ts = TimeSpan.FromMilliseconds(totalMs);
            Hours = ts.Hours;
            Minutes = ts.Minutes;
            Seconds = ts.Seconds;
            Milliseconds = ts.Milliseconds;
        }

        public TimeSpan ToTimeSpan()
        {
            return new TimeSpan(0, Hours, Minutes, Seconds, Milliseconds);
        }
    }

    internal class Subtitle
    {
        private List<Paragraph> _paragraphs;
        private readonly SubRip _format;

        public string Header { get; set; }
        public string Footer { get; set; }
        public string FileName { get; set; }
        public bool IsHearingImpaired { get; private set; }
        public List<Paragraph> Paragraphs => _paragraphs;

        public Subtitle(SubRip subrip)
        {
            _paragraphs = new List<Paragraph>();
            FileName = "Untitled";
            _format = subrip;
        }

        public Paragraph GetParagraphOrDefault(int index)
        {
            if (_paragraphs == null || _paragraphs.Count <= index || index < 0)
                return null;
            return _paragraphs[index];
        }

        public string ToText()
        {
            return _format.ToText(this, System.IO.Path.GetFileNameWithoutExtension(FileName));
        }

        public void AddTimeToAllParagraphs(TimeSpan time)
        {
            foreach (var paragraph in Paragraphs)
            {
                paragraph.StartTime.AddTime(time);
                paragraph.EndTime.AddTime(time);
            }
        }

        public void Renumber(int startNumber)
        {
            int num = ((startNumber >= 0) ? startNumber : 0);
            foreach (var paragraph in _paragraphs)
            {
                paragraph.Number = num;
                num++;
            }
        }
    }

    internal class SubRip
    {
        private enum ExpectingLine
        {
            Number,
            TimeCodes,
            Text
        }

        private static Regex RegexTimeCodes = new Regex(@"^\d{1,2}:\d{2}:\d{2}[,.:]\d{1,3}\s*-->\s*\d{1,2}:\d{2}:\d{2}[,.:]\d{1,3}$", RegexOptions.Compiled);

        private ExpectingLine _expecting;
        private Paragraph _paragraph;
        private int _errorCount;

        public string Extension => ".srt";
        public string Name => "SubRip";
        public int ErrorCount => _errorCount;

        public void LoadSubtitle(Subtitle subtitle, List<string> lines, string fileName)
        {
            bool flag = false;
            _paragraph = new Paragraph();
            _expecting = ExpectingLine.Number;
            _errorCount = 0;
            subtitle.Paragraphs.Clear();

            for (int i = 0; i < lines.Count; i++)
            {
                string text = lines[i].TrimEnd();
                text = text.Trim('\u007f');
                string next = (i + 1 < lines.Count) ? lines[i + 1] : string.Empty;

                if (_expecting == ExpectingLine.Text && i + 1 < lines.Count && _paragraph != null && !string.IsNullOrEmpty(_paragraph.Text) && int.TryParse(text, out _) && RegexTimeCodes.IsMatch(lines[i + 1]))
                {
                    _errorCount++;
                    ReadLine(subtitle, string.Empty, string.Empty);
                }
                if (_expecting == ExpectingLine.Number && RegexTimeCodes.IsMatch(text))
                {
                    _errorCount++;
                    _expecting = ExpectingLine.TimeCodes;
                    flag = true;
                }
                ReadLine(subtitle, text, next);
            }

            if (_paragraph.Text.Trim().Length > 0)
                subtitle.Paragraphs.Add(_paragraph);

            foreach (var paragraph in subtitle.Paragraphs)
                paragraph.Text = paragraph.Text.Replace(Environment.NewLine + Environment.NewLine, Environment.NewLine);

            if (_errorCount < 100 && flag)
                subtitle.Renumber(1);
        }

        public string ToText(Subtitle subtitle, string title)
        {
            var sb = new StringBuilder();
            foreach (var paragraph in subtitle.Paragraphs)
            {
                var text = paragraph.Text.Replace(Environment.NewLine + Environment.NewLine, Environment.NewLine);
                sb.Append($"{paragraph.Number}\r\n{paragraph.StartTime} --> {paragraph.EndTime}\r\n{text}\r\n\r\n");
            }
            return sb.ToString().Trim();
        }

        private void ReadLine(Subtitle subtitle, string line, string next)
        {
            switch (_expecting)
            {
                case ExpectingLine.Number:
                    if (int.TryParse(line, out int number))
                    {
                        _paragraph.Number = number;
                        _expecting = ExpectingLine.TimeCodes;
                    }
                    else if (line.Trim().Length > 0)
                    {
                        _errorCount++;
                    }
                    break;
                case ExpectingLine.TimeCodes:
                    if (TryReadTimeCodesLine(line, _paragraph))
                    {
                        _paragraph.Text = string.Empty;
                        _expecting = ExpectingLine.Text;
                    }
                    else if (line.Trim().Length > 0)
                    {
                        _errorCount++;
                        _expecting = ExpectingLine.Number;
                    }
                    break;
                case ExpectingLine.Text:
                    if (!string.IsNullOrWhiteSpace(line))
                    {
                        if (_paragraph.Text.Length > 0)
                            _paragraph.Text += Environment.NewLine;

                        _paragraph.Text += line.TrimEnd().Replace(Environment.NewLine + Environment.NewLine, Environment.NewLine);
                    }
                    else if (!string.IsNullOrWhiteSpace(next) && (int.TryParse(next, out _) || RegexTimeCodes.IsMatch(next)))
                    {
                        subtitle.Paragraphs.Add(_paragraph);
                        _paragraph = new Paragraph();
                        _expecting = ExpectingLine.Number;
                    }
                    break;
            }
        }

        private bool TryReadTimeCodesLine(string line, Paragraph paragraph)
        {
            line = line.Replace('،', ',').Replace('\uf7c8', ',').Replace('¡', ',');
            line = line.Replace(" -> ", " --> ").Replace(" - > ", " --> ").Replace(" ->> ", " --> ");
            line = line.Replace(" -- > ", " --> ").Replace(" - -> ", " --> ").Replace(" -->> ", " --> ").Replace(" ---> ", " --> ");

            line = line.Trim();
            line = line.Replace('.', ':');
            line = Regex.Replace(line, @"\s+", " ");

            if (!RegexTimeCodes.IsMatch(line))
                return false;

            string[] parts = line.Split(new[] { ' ', '-', '>', ':', ',' }, StringSplitOptions.RemoveEmptyEntries);
            try
            {
                int h1 = int.Parse(parts[0]);
                int m1 = int.Parse(parts[1]);
                int s1 = int.Parse(parts[2]);
                int ms1 = int.Parse(parts[3]);

                int h2 = int.Parse(parts[4]);
                int m2 = int.Parse(parts[5]);
                int s2 = int.Parse(parts[6]);
                int ms2 = int.Parse(parts[7]);

                paragraph.StartTime = new TimeCode(h1, m1, s1, ms1);
                paragraph.EndTime = new TimeCode(h2, m2, s2, ms2);

                return true;
            }
            catch
            {
                return false;
            }
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions