feature(subtitles): add frog_subtitles addon

This commit is contained in:
2025-09-23 00:16:43 +02:00
parent 6c8d542e53
commit d3eff4e006
21 changed files with 371 additions and 2 deletions

View File

@@ -0,0 +1,176 @@
#if TOOLS
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Godot;
using Godot.Collections;
namespace Frog;
public partial class SubtitlesImportPlugin : EditorImportPlugin
{
private readonly static Regex timeRegex = new Regex(@"(?<start>[0-9,:\.]+)\ -->\ (?<end>[0-9,:\.]+)");
private readonly static Regex colorRegex = new Regex(@"<font color=""(.+)"">");
private readonly static Regex linePositionRegex = new Regex(@"\{\\a([0-9]+)\}");
public override string _GetImporterName() => "frog-subtitles.plugin";
public override string _GetVisibleName() => "Frog Subtitles";
public override string[] _GetRecognizedExtensions() => new string[] { "srt" };
public override string _GetSaveExtension() => "res";
public override string _GetResourceType() => "Resource";
public override int _GetPresetCount() => 1;
public override float _GetPriority() => 1;
public override int _GetImportOrder() => 0;
public override string _GetPresetName(int presetIndex) => "Default";
public override Array<Dictionary> _GetImportOptions(string path, int presetIndex)
{
return new Array<Dictionary>();
}
public override Error _Import(string sourceFile, string savePath, Dictionary options, Array<string> platformVariants, Array<string> genFiles)
{
Error parseResult = Error.Ok;
ReaderState state = ReaderState.ReadId;
List<SubtitleEntry> entries = new();
try
{
int currentId = 0;
TimeSpan startTime = TimeSpan.Zero;
TimeSpan endTime = TimeSpan.Zero;
StringBuilder content = new();
void RegisterEntry()
{
content.Replace("<b>", "[b]").Replace("</b>", "[/b]").Replace("{b}", "[b]").Replace("{/b}", "[/b]");
content.Replace("<i>", "[i]").Replace("</i>", "[/i]").Replace("{i}", "[i]").Replace("{/i}", "[/i]");
content.Replace("<u>", "[u]").Replace("</u>", "[/u]").Replace("{u}", "[u]").Replace("{/u}", "[/u]");
content.Replace("</font>", "[/color]");
string contentString = content.ToString();
contentString = colorRegex.Replace(contentString, match => $"[color={match.Groups[1].Value}]");
contentString = linePositionRegex.Replace(contentString, match =>
{
string result = string.Empty;
if (int.TryParse(match.Groups[1].Value, out int lineCount))
{
for (int i = 1; i < lineCount; ++i)
{
result += '\n';
}
}
else
{
Trace.TraceError($"Can't parse subtitles line position tag: {match.Value}");
}
return result;
});
entries.Add(new SubtitleEntry()
{
Id = currentId,
StartTime = startTime.TotalSeconds,
EndTime = endTime.TotalSeconds,
Content = contentString,
});
content.Clear();
}
using (StreamReader reader = File.OpenText(ProjectSettings.GlobalizePath(sourceFile)))
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
switch (state)
{
case ReaderState.ReadId:
if (!int.TryParse(line, out currentId))
{
Trace.TraceError($"Can't parse subtitles id: {line}");
parseResult = Error.Failed;
}
state = ReaderState.ReadTime;
break;
case ReaderState.ReadTime:
Match match = SubtitlesImportPlugin.timeRegex.Match(line);
if (!match.Success ||
!TimeSpan.TryParse(match.Groups["start"].Value.Replace(',', '.'), out startTime) ||
!TimeSpan.TryParse(match.Groups["end"].Value.Replace(',', '.'), out endTime))
{
Trace.TraceError($"Can't parse subtitles time: {line}");
parseResult = Error.Failed;
}
state = ReaderState.ReadContent;
break;
case ReaderState.ReadContent:
if (string.IsNullOrEmpty(line))
{
// End of current entry. Store it and clear buffers.
RegisterEntry();
state = ReaderState.ReadId;
break;
}
if (content.Length > 0)
{
content.Append('\n');
}
content.Append(line);
break;
}
}
// Register last entry (no need to end a line, end of file is enough)
if (content.Length > 0)
{
RegisterEntry();
}
}
}
catch
{
return Error.Failed;
}
if (parseResult != Error.Ok)
{
return parseResult;
}
Debug.WriteLine($"{sourceFile} parsed. Found {entries.Count} subtitles entries.");
entries.Sort((left, right) => left.Id.CompareTo(right.Id));
var subtitles = new Subtitles()
{
Entries = new Array<SubtitleEntry>(entries),
};
string filename = $"{savePath}.{this._GetSaveExtension()}";
return ResourceSaver.Save(subtitles, filename);
}
private enum ReaderState
{
ReadId,
ReadTime,
ReadContent,
}
}
#endif

View File

@@ -0,0 +1 @@
uid://cp8l0est28gtv

View File

@@ -0,0 +1,34 @@
#if TOOLS
using Godot;
namespace Frog;
[Tool]
public partial class SubtitlesPlugin : EditorPlugin
{
private SubtitlesImportPlugin? importPlugin;
public override void _EnterTree()
{
Script videoStreamSubtitlesScript = GD.Load<Script>("res://addons/frog_subtitles/nodes/VideoStreamSubtitles.cs");
Texture2D videoStreamSubtitlesIcon = GD.Load<Texture2D>("res://addons/frog_subtitles/icons/video_subtitles.svg");
this.AddCustomType(nameof(VideoStreamSubtitles), nameof(RichTextLabel), videoStreamSubtitlesScript, videoStreamSubtitlesIcon);
Script audioStreamSubtitlesScript = GD.Load<Script>("res://addons/frog_subtitles/nodes/AudioStreamSubtitles.cs");
Texture2D audioStreamSubtitlesIcon = GD.Load<Texture2D>("res://addons/frog_subtitles/icons/audio_subtitles.svg");
this.AddCustomType(nameof(AudioStreamSubtitles), nameof(RichTextLabel), audioStreamSubtitlesScript, audioStreamSubtitlesIcon);
this.importPlugin = new SubtitlesImportPlugin();
this.AddImportPlugin(this.importPlugin);
}
public override void _ExitTree()
{
this.RemoveImportPlugin(this.importPlugin);
this.importPlugin = null;
this.RemoveCustomType(nameof(AudioStreamSubtitles));
this.RemoveCustomType(nameof(VideoStreamSubtitles));
}
}
#endif

View File

@@ -0,0 +1 @@
uid://dv6cp0hj0qxir

View File

@@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><linearGradient id="a" gradientUnits="userSpaceOnUse" x2="0" y1="1" y2="15"><stop offset="0" stop-color="#ff5f5f"/><stop offset=".5" stop-color="#e1da5b"/><stop offset="1" stop-color="#5fff97"/></linearGradient><path d="M9 14a1 1 0 0 0 1.5.85l4-2.511a1 1 0 0 0 0-1.724l-4-2.511a1 1 0 0 0-1.5.85z" fill="#e0e0e0"/><path d="M13 2a1 1 0 0 0-1-1L4.754 3A1 1 0 0 0 4 4v5.55A2.5 2.5 0 1 0 6 12V4.756l5-1.428V6.5l2-1z" fill="url(#a)"/></svg>

After

Width:  |  Height:  |  Size: 518 B

View File

@@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M3 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm0 2h10v8H3zm3 2v4l4-2z" fill="#8eef97"/></svg>

After

Width:  |  Height:  |  Size: 209 B

View File

@@ -0,0 +1,16 @@
using Godot;
namespace Frog;
public partial class AudioStreamSubtitles : StreamSubtitles
{
[Export] private AudioStreamPlayer? player;
public override void _Process(double delta)
{
if (this.player.Playing)
{
this.UpdateContent(this.player.GetPlaybackPosition());
}
}
}

View File

@@ -0,0 +1 @@
uid://dlduoft2kspcc

View File

@@ -0,0 +1,53 @@
using System.Diagnostics;
using Godot;
namespace Frog;
public abstract partial class StreamSubtitles : RichTextLabel
{
[Export] private Subtitles? subtitles;
private SubtitleEntry? currentEntry;
private string template;
public override void _Ready()
{
Debug.Assert(this.subtitles != null);
this.template = this.Text;
this.Text = string.Empty;
}
protected void UpdateContent(double currentTime)
{
if (this.currentEntry != null && currentTime > this.currentEntry.EndTime)
{
this.currentEntry = null;
this.Text = string.Empty;
}
if (this.currentEntry == null)
{
// Search for a valid entry...
foreach (SubtitleEntry entry in this.subtitles.Entries)
{
if (currentTime >= entry.StartTime && currentTime <= entry.EndTime)
{
this.currentEntry = entry;
break;
}
}
if (this.currentEntry != null)
{
if (string.IsNullOrEmpty(this.template))
{
this.Text = this.currentEntry.Content;
}
else
{
this.Text = string.Format(this.template, this.currentEntry.Content);
}
}
}
}
}

View File

@@ -0,0 +1 @@
uid://losknse77c4b

View File

@@ -0,0 +1,16 @@
using Godot;
namespace Frog;
public partial class VideoStreamSubtitles : StreamSubtitles
{
[Export] private VideoStreamPlayer? player;
public override void _Process(double delta)
{
if (this.player.Visible && this.player.IsPlaying())
{
this.UpdateContent(this.player.StreamPosition);
}
}
}

View File

@@ -0,0 +1 @@
uid://bwj1s8v1ceta

View File

@@ -0,0 +1,7 @@
[plugin]
name="Frog Subtitles"
description="Custom nodes made to add subtitles to an AudioStream or a VideoStream directly using standard srt files."
author="Frog Collective"
version="1.1.0"
script="SubtitlesPlugin.cs"

View File

@@ -0,0 +1,11 @@
using Godot;
namespace Frog;
public partial class SubtitleEntry : Resource
{
[Export] public int Id;
[Export] public double StartTime;
[Export] public double EndTime;
[Export] public string Content;
}

View File

@@ -0,0 +1 @@
uid://d4c1aqagv0d26

View File

@@ -0,0 +1,9 @@
using Godot;
using Godot.Collections;
namespace Frog;
public partial class Subtitles : Resource
{
[Export] public Array<SubtitleEntry> Entries;
}

View File

@@ -0,0 +1 @@
uid://bnr4d41k74dvk