Skip to content

Commit c8da4e0

Browse files
committed
update sdk site
1 parent 5c30eb8 commit c8da4e0

File tree

26 files changed

+23278
-100
lines changed

26 files changed

+23278
-100
lines changed

.claude/launch.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
"configurations": [
44
{
55
"name": "tps-site",
6-
"runtimeExecutable": "/usr/local/bin/node",
7-
"runtimeArgs": ["/opt/homebrew/bin/npx", "serve", "dist", "-l", "3456", "--no-clipboard"],
6+
"runtimeExecutable": "/bin/bash",
7+
"runtimeArgs": ["/Users/ksemenenko/Developer/TPS/.claude/serve.sh"],
88
"port": 3456
99
}
1010
]

.claude/serve.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
export PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"
3+
npx serve dist -l 3456 --no-clipboard

.github/workflows/sdk-typescript.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,4 @@ jobs:
3232
with:
3333
node-version: 22
3434
- run: npm ci --prefix SDK/js
35-
- run: npm --prefix SDK/js run build:tps
36-
- run: npm --prefix SDK/js run test:types
35+
- run: npm --prefix SDK/js run test:typescript

SDK/README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ The TypeScript runtime is the canonical implementation. The JavaScript runtime i
3939

4040
| Folder | Purpose | Edit Here When | Main Commands |
4141
|--------|---------|----------------|---------------|
42-
| `SDK/ts` | canonical runtime source | changing TPS behavior or runtime contract | `npm --prefix SDK/js run build:tps`, `npm --prefix SDK/js run test:types` |
42+
| `SDK/ts` | canonical runtime source | changing TPS behavior or runtime contract | `npm --prefix SDK/js run build:tps`, `npm --prefix SDK/js run test:typescript` |
4343
| `SDK/js` | JavaScript package and Node validation | changing JS packaging or JS-specific tests | `npm --prefix SDK/js run test:js`, `npm --prefix SDK/js run coverage:js` |
4444
| `SDK/dotnet` | C# runtime and tests | changing .NET API or .NET behavior | `dotnet build SDK/dotnet/ManagedCode.Tps.slnx -warnaserror --no-restore`, `dotnet test SDK/dotnet/ManagedCode.Tps.slnx --no-build --no-restore` |
4545
| `SDK/flutter` | placeholder | starting Flutter implementation | define runtime structure, tests, and workflow |
@@ -64,13 +64,22 @@ Each compiled word carries timing and authoring-derived metadata such as emphasi
6464
2. Rebuild the JS runtime from the TS source.
6565
3. Run the runtime-specific tests for the SDK you changed.
6666
4. If behavior changes, keep parity across active runtimes and shared fixtures.
67+
5. Regenerate example snapshots when the compiled output or player states intentionally change.
6768

6869
## Local Verification
6970

70-
- TypeScript: `npm --prefix SDK/js run test:types`
71+
- TypeScript: `npm --prefix SDK/js run test:typescript`
7172
- JavaScript: `npm --prefix SDK/js run coverage:js`
7273
- C#: `dotnet test SDK/dotnet/ManagedCode.Tps.slnx --no-build --no-restore /p:CollectCoverage=true /p:CoverletOutputFormat=json /p:ThresholdType=line%2Cbranch%2Cmethod /p:Threshold=90`
7374

75+
## Shared Example Snapshots
76+
77+
`SDK/fixtures/examples/*.snapshot.json` are cross-runtime integration fixtures generated from the documented `examples/*.tps` files. Active runtimes must compile those examples into the same normalized state machine shape and produce the same checkpointed player states.
78+
79+
Regenerate them with:
80+
81+
- `npm --prefix SDK/js run generate:example-snapshots`
82+
7483
## GitHub Workflows
7584

7685
- `.github/workflows/ci.yml`: repo-wide build/test matrix

SDK/dotnet/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ This is the project to change when the .NET API, serialization behavior, or .NET
2020
- `TpsRuntime.Parse(source)`
2121
- `TpsRuntime.Compile(source)`
2222
- `TpsPlayer`
23+
- `TpsPlayer.EnumerateStates(stepMs)`
2324

2425
## Project Layout
2526

@@ -38,6 +39,7 @@ This is the project to change when the .NET API, serialization behavior, or .NET
3839
1. Update `src/ManagedCode.Tps/` for runtime or API changes.
3940
2. Keep behavior aligned with the active TPS contract used by the TS/JS SDKs.
4041
3. Run build, tests, and coverage checks after changes.
42+
4. Keep example snapshot parity with the shared fixtures under `SDK/fixtures/examples`.
4143

4244
## Local Commands
4345

SDK/dotnet/src/ManagedCode.Tps/TpsPlayer.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,24 @@ public PlayerState GetState(int elapsedMs)
7575

7676
public PlayerState Seek(int elapsedMs) => GetState(elapsedMs);
7777

78+
public IEnumerable<PlayerState> EnumerateStates(int stepMs = 100)
79+
{
80+
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(stepMs);
81+
82+
if (Script.TotalDurationMs == 0)
83+
{
84+
yield return GetState(0);
85+
yield break;
86+
}
87+
88+
for (var elapsedMs = 0; elapsedMs < Script.TotalDurationMs; elapsedMs += stepMs)
89+
{
90+
yield return GetState(elapsedMs);
91+
}
92+
93+
yield return GetState(Script.TotalDurationMs);
94+
}
95+
7896
private CompiledWord? FindCurrentWord(int elapsedMs)
7997
{
8098
if (Script.Words.Count == 0)
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
using System.Globalization;
2+
using System.Text.Json;
3+
using System.Text.Json.Nodes;
4+
using ManagedCode.Tps.Models;
5+
6+
namespace ManagedCode.Tps.Tests;
7+
8+
public sealed class ExampleSnapshotTests
9+
{
10+
private static readonly JsonSerializerOptions JsonOptions = new()
11+
{
12+
WriteIndented = true
13+
};
14+
15+
[Fact]
16+
public void Compile_Examples_MatchSharedCompiledAndPlayerSnapshots()
17+
{
18+
foreach (var fileName in ExampleFiles)
19+
{
20+
var source = File.ReadAllText(Path.Combine(ExamplesRoot, fileName));
21+
var result = TpsRuntime.Compile(source);
22+
23+
Assert.True(result.Ok, fileName);
24+
25+
var expected = JsonNode.Parse(File.ReadAllText(Path.Combine(ExampleSnapshotsRoot, $"{Path.GetFileNameWithoutExtension(fileName)}.snapshot.json")));
26+
var actual = BuildExampleSnapshot(fileName, result.Script);
27+
28+
Assert.True(
29+
JsonNode.DeepEquals(expected, actual),
30+
$"Snapshot mismatch for {fileName}{Environment.NewLine}Expected:{Environment.NewLine}{expected!.ToJsonString(JsonOptions)}{Environment.NewLine}{Environment.NewLine}Actual:{Environment.NewLine}{actual.ToJsonString(JsonOptions)}");
31+
}
32+
}
33+
34+
[Fact]
35+
public void Player_EnumerateStates_WalksThePlaybackTimeline()
36+
{
37+
var compiled = TpsRuntime.Compile(File.ReadAllText(Path.Combine(ExamplesRoot, "basic.tps"))).Script;
38+
var player = new TpsPlayer(compiled);
39+
40+
var states = player.EnumerateStates(Math.Max(1, compiled.TotalDurationMs / 4)).ToArray();
41+
42+
Assert.True(states.Length >= 2);
43+
Assert.Equal(0, states[0].ElapsedMs);
44+
Assert.Equal(compiled.TotalDurationMs, states[^1].ElapsedMs);
45+
Assert.True(states[^1].IsComplete);
46+
Assert.Throws<ArgumentOutOfRangeException>(() => player.EnumerateStates(0).ToArray());
47+
}
48+
49+
private static JsonObject BuildExampleSnapshot(string fileName, CompiledScript script)
50+
{
51+
var player = new TpsPlayer(script);
52+
var checkpoints = new JsonArray();
53+
foreach (var checkpoint in CreateCheckpointTimes(script.TotalDurationMs))
54+
{
55+
checkpoints.Add(NormalizePlayerState(checkpoint.Label, player.GetState(checkpoint.ElapsedMs)));
56+
}
57+
58+
return new JsonObject
59+
{
60+
["fileName"] = fileName,
61+
["source"] = $"examples/{fileName}",
62+
["compiled"] = NormalizeCompiledScript(script),
63+
["player"] = new JsonObject
64+
{
65+
["checkpoints"] = checkpoints
66+
}
67+
};
68+
}
69+
70+
private static JsonObject NormalizeCompiledScript(CompiledScript script)
71+
{
72+
var metadata = new JsonObject();
73+
foreach (var entry in script.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
74+
{
75+
metadata[entry.Key] = entry.Value;
76+
}
77+
78+
var segments = new JsonArray();
79+
foreach (var segment in script.Segments)
80+
{
81+
segments.Add(NormalizeCompiledSegment(segment));
82+
}
83+
84+
var words = new JsonArray();
85+
foreach (var word in script.Words)
86+
{
87+
words.Add(NormalizeCompiledWord(word));
88+
}
89+
90+
return new JsonObject
91+
{
92+
["metadata"] = metadata,
93+
["totalDurationMs"] = script.TotalDurationMs,
94+
["segments"] = segments,
95+
["words"] = words
96+
};
97+
}
98+
99+
private static JsonObject NormalizeCompiledSegment(CompiledSegment segment)
100+
{
101+
var blocks = new JsonArray();
102+
foreach (var block in segment.Blocks)
103+
{
104+
blocks.Add(NormalizeCompiledBlock(block));
105+
}
106+
107+
return Compact(new JsonObject
108+
{
109+
["id"] = segment.Id,
110+
["name"] = segment.Name,
111+
["targetWpm"] = segment.TargetWpm,
112+
["emotion"] = segment.Emotion,
113+
["speaker"] = segment.Speaker,
114+
["timing"] = segment.Timing,
115+
["backgroundColor"] = segment.BackgroundColor,
116+
["textColor"] = segment.TextColor,
117+
["accentColor"] = segment.AccentColor,
118+
["startWordIndex"] = segment.StartWordIndex,
119+
["endWordIndex"] = segment.EndWordIndex,
120+
["startMs"] = segment.StartMs,
121+
["endMs"] = segment.EndMs,
122+
["wordIds"] = ToJsonArray(segment.Words.Select(word => word.Id)),
123+
["blocks"] = blocks
124+
});
125+
}
126+
127+
private static JsonObject NormalizeCompiledBlock(CompiledBlock block)
128+
{
129+
var phrases = new JsonArray();
130+
foreach (var phrase in block.Phrases)
131+
{
132+
phrases.Add(NormalizeCompiledPhrase(phrase));
133+
}
134+
135+
return Compact(new JsonObject
136+
{
137+
["id"] = block.Id,
138+
["name"] = block.Name,
139+
["targetWpm"] = block.TargetWpm,
140+
["emotion"] = block.Emotion,
141+
["speaker"] = block.Speaker,
142+
["isImplicit"] = block.IsImplicit,
143+
["startWordIndex"] = block.StartWordIndex,
144+
["endWordIndex"] = block.EndWordIndex,
145+
["startMs"] = block.StartMs,
146+
["endMs"] = block.EndMs,
147+
["wordIds"] = ToJsonArray(block.Words.Select(word => word.Id)),
148+
["phrases"] = phrases
149+
});
150+
}
151+
152+
private static JsonObject NormalizeCompiledPhrase(CompiledPhrase phrase) =>
153+
Compact(new JsonObject
154+
{
155+
["id"] = phrase.Id,
156+
["text"] = phrase.Text,
157+
["startWordIndex"] = phrase.StartWordIndex,
158+
["endWordIndex"] = phrase.EndWordIndex,
159+
["startMs"] = phrase.StartMs,
160+
["endMs"] = phrase.EndMs,
161+
["wordIds"] = ToJsonArray(phrase.Words.Select(word => word.Id))
162+
});
163+
164+
private static JsonObject NormalizeCompiledWord(CompiledWord word) =>
165+
Compact(new JsonObject
166+
{
167+
["id"] = word.Id,
168+
["index"] = word.Index,
169+
["kind"] = word.Kind,
170+
["cleanText"] = word.CleanText,
171+
["characterCount"] = word.CharacterCount,
172+
["orpPosition"] = word.OrpPosition,
173+
["displayDurationMs"] = word.DisplayDurationMs,
174+
["startMs"] = word.StartMs,
175+
["endMs"] = word.EndMs,
176+
["metadata"] = NormalizeWordMetadata(word.Metadata),
177+
["segmentId"] = word.SegmentId,
178+
["blockId"] = word.BlockId,
179+
["phraseId"] = word.PhraseId
180+
});
181+
182+
private static JsonObject NormalizeWordMetadata(WordMetadata metadata) =>
183+
Compact(new JsonObject
184+
{
185+
["isEmphasis"] = metadata.IsEmphasis,
186+
["emphasisLevel"] = metadata.EmphasisLevel,
187+
["isPause"] = metadata.IsPause,
188+
["pauseDurationMs"] = metadata.PauseDurationMs,
189+
["isHighlight"] = metadata.IsHighlight,
190+
["isBreath"] = metadata.IsBreath,
191+
["isEditPoint"] = metadata.IsEditPoint,
192+
["editPointPriority"] = metadata.EditPointPriority,
193+
["emotionHint"] = metadata.EmotionHint,
194+
["inlineEmotionHint"] = metadata.InlineEmotionHint,
195+
["volumeLevel"] = metadata.VolumeLevel,
196+
["deliveryMode"] = metadata.DeliveryMode,
197+
["phoneticGuide"] = metadata.PhoneticGuide,
198+
["pronunciationGuide"] = metadata.PronunciationGuide,
199+
["stressText"] = metadata.StressText,
200+
["stressGuide"] = metadata.StressGuide,
201+
["speedOverride"] = metadata.SpeedOverride,
202+
["speedMultiplier"] = metadata.SpeedMultiplier is null ? null : NormalizeNumber(metadata.SpeedMultiplier.Value),
203+
["speaker"] = metadata.Speaker,
204+
["headCue"] = metadata.HeadCue
205+
});
206+
207+
private static JsonObject NormalizePlayerState(string label, PlayerState state) =>
208+
Compact(new JsonObject
209+
{
210+
["label"] = label,
211+
["elapsedMs"] = state.ElapsedMs,
212+
["remainingMs"] = state.RemainingMs,
213+
["progress"] = NormalizeNumber(state.Progress),
214+
["isComplete"] = state.IsComplete,
215+
["currentWordIndex"] = state.CurrentWordIndex,
216+
["currentWordId"] = state.CurrentWord?.Id,
217+
["currentWordText"] = state.CurrentWord?.CleanText,
218+
["currentWordKind"] = state.CurrentWord?.Kind,
219+
["previousWordId"] = state.PreviousWord?.Id,
220+
["nextWordId"] = state.NextWord?.Id,
221+
["currentSegmentId"] = state.CurrentSegment?.Id,
222+
["currentBlockId"] = state.CurrentBlock?.Id,
223+
["currentPhraseId"] = state.CurrentPhrase?.Id,
224+
["nextTransitionMs"] = state.NextTransitionMs,
225+
["presentation"] = Compact(new JsonObject
226+
{
227+
["segmentName"] = state.Presentation.SegmentName,
228+
["blockName"] = state.Presentation.BlockName,
229+
["phraseText"] = state.Presentation.PhraseText,
230+
["visibleWordIds"] = ToJsonArray(state.Presentation.VisibleWords.Select(word => word.Id)),
231+
["visibleWordTexts"] = ToJsonArray(state.Presentation.VisibleWords.Select(word => word.CleanText)),
232+
["activeWordInPhrase"] = state.Presentation.ActiveWordInPhrase
233+
})
234+
});
235+
236+
private static JsonArray ToJsonArray(IEnumerable<string> values)
237+
{
238+
var array = new JsonArray();
239+
foreach (var value in values)
240+
{
241+
array.Add(value);
242+
}
243+
244+
return array;
245+
}
246+
247+
private static JsonObject Compact(JsonObject value)
248+
{
249+
var toRemove = new List<string>();
250+
foreach (var property in value)
251+
{
252+
if (property.Value is null)
253+
{
254+
toRemove.Add(property.Key);
255+
continue;
256+
}
257+
258+
if (property.Value is JsonObject childObject)
259+
{
260+
Compact(childObject);
261+
}
262+
}
263+
264+
foreach (var propertyName in toRemove)
265+
{
266+
value.Remove(propertyName);
267+
}
268+
269+
return value;
270+
}
271+
272+
private static double NormalizeNumber(double value) =>
273+
double.Parse(value.ToString("0.000000", CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
274+
275+
private static IEnumerable<(string Label, int ElapsedMs)> CreateCheckpointTimes(int totalDurationMs)
276+
{
277+
var checkpoints = new[]
278+
{
279+
("start", 0),
280+
("quarter", (int)Math.Round(totalDurationMs * 0.25d, MidpointRounding.AwayFromZero)),
281+
("middle", (int)Math.Round(totalDurationMs * 0.5d, MidpointRounding.AwayFromZero)),
282+
("threeQuarter", (int)Math.Round(totalDurationMs * 0.75d, MidpointRounding.AwayFromZero)),
283+
("complete", totalDurationMs)
284+
};
285+
286+
var seen = new HashSet<int>();
287+
foreach (var checkpoint in checkpoints)
288+
{
289+
if (seen.Add(checkpoint.Item2))
290+
{
291+
yield return checkpoint;
292+
}
293+
}
294+
}
295+
296+
private static string ExamplesRoot =>
297+
Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../../../examples"));
298+
299+
private static string ExampleSnapshotsRoot =>
300+
Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../../../SDK/fixtures/examples"));
301+
302+
private static string[] ExampleFiles => ["basic.tps", "advanced.tps", "multi-segment.tps"];
303+
}

0 commit comments

Comments
 (0)