Skip to content

Commit fbd9016

Browse files
committed
chore(merge): merge master into chore/remove-dead-jit-scaffolding
Resolves conflicts: - Directory.Packages.props: take AiDotNet.Tensors 0.46.1 from master, keep AiDotNet.Native.OneDNN 0.46.0 for ecosystem alignment. - src/NeuralNetworks/MobileNetV2Network.cs: keep both overrides -- GetNamedLayerActivations (master, 3D->4D probe shape) and PredictEager (HEAD, compiled-plan routing). Master's Predict override was superseded by PredictEager + base-class routing.
2 parents 7e5236a + 15c6f47 commit fbd9016

45 files changed

Lines changed: 1950 additions & 496 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy-website.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,28 @@ jobs:
5959
- name: Install dependencies
6060
run: npm ci
6161

62+
- name: Verify required build secrets
63+
env:
64+
PUBLIC_SUPABASE_URL: ${{ secrets.PUBLIC_SUPABASE_URL }}
65+
PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.PUBLIC_SUPABASE_ANON_KEY }}
66+
run: |
67+
# Fail the job here rather than deploying a site that silently
68+
# disables auth. Astro bakes these at build time — a missing or
69+
# rotated secret would otherwise produce a broken production bundle.
70+
: "${PUBLIC_SUPABASE_URL:?Missing PUBLIC_SUPABASE_URL GitHub secret}"
71+
: "${PUBLIC_SUPABASE_ANON_KEY:?Missing PUBLIC_SUPABASE_ANON_KEY GitHub secret}"
72+
6273
- name: Build website
6374
run: npx astro build
6475
env:
6576
VERCEL: '1'
77+
# Astro bakes import.meta.env.PUBLIC_* values at build time. Without these
78+
# the client-side Supabase client throws "Missing Supabase environment
79+
# variables" at page load, which in turn means every script that imports
80+
# src/lib/supabase.ts crashes — sign-in buttons silently become no-ops.
81+
# Keep these in lockstep with the Vercel project env vars.
82+
PUBLIC_SUPABASE_URL: ${{ secrets.PUBLIC_SUPABASE_URL }}
83+
PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.PUBLIC_SUPABASE_ANON_KEY }}
6684

6785
- name: Verify build output
6886
run: |

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<ItemGroup>
66
<!-- AiDotNet ecosystem -->
77
<PackageVersion Include="AiDotNet" Version="0.113.0" />
8-
<PackageVersion Include="AiDotNet.Tensors" Version="0.46.0" />
8+
<PackageVersion Include="AiDotNet.Tensors" Version="0.46.1" />
99
<PackageVersion Include="AiDotNet.Native.OneDNN" Version="0.46.0" />
1010
<PackageVersion Include="AiDotNet.Native.OpenBLAS" Version="0.28.0" />
1111
<PackageVersion Include="AiDotNet.Native.CLBlast" Version="0.37.0" />

src/AiDotNet.Generators/TestScaffoldGenerator.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,14 +1492,21 @@ private static void EmitGeneratedTestClass(
14921492
}
14931493
}
14941494
}
1495-
else if (model.Domains.Contains(3) && !model.Tasks.Contains(35))
1495+
else if (model.Domains.Contains(4) && !model.Tasks.Contains(35))
14961496
{
14971497
// Temporal video models (ActionRecognition=22, VideoGeneration=41, etc.)
14981498
// need a 4D [frames, channels, height, width] input shape, but
14991499
// NeuralNetworkArchitecture only expresses 3D (height/width/depth).
15001500
// Rather than silently emit a mismatched 3D architecture alongside a
15011501
// 4D InputShape, route these to a runtime placeholder until the
15021502
// architecture type can represent a temporal dimension.
1503+
//
1504+
// The enum ordinal for ModelDomain.Video is 4
1505+
// (General=0, Vision=1, Language=2, Audio=3, Video=4, ...).
1506+
// This check previously used 3, which incorrectly flagged every
1507+
// *audio* model (PlayHT, Bark, etc.) as "temporal video" and
1508+
// emitted a NotImplementedException factory — ten PlayHTTests
1509+
// failures on PR #1156 traced to this off-by-one.
15031510
constructorExpr = "throw new System.NotImplementedException(" +
15041511
$"\"'{GeneratorHelpers.StripGenericSuffix(model.ClassName)}' is a temporal video model; NeuralNetworkArchitecture<T> cannot express its 4D [frames, channels, height, width] input. Implement this factory manually.\")";
15051512
}
@@ -1510,7 +1517,7 @@ private static void EmitGeneratedTestClass(
15101517
// others default to OneDimensional. Temporal video is handled above.
15111518
needsArchitectureUsing = true;
15121519
bool isVision = model.Domains.Contains(1) || model.Domains.Contains(11); // Vision=1, ThreeD=11
1513-
bool isAudio = model.Domains.Contains(4); // Audio=4
1520+
bool isAudio = model.Domains.Contains(3); // Audio=3 (enum ordinal, not Video=4)
15141521
bool isFrameInterp = model.Tasks.Contains(35); // FrameInterpolation → 3D input
15151522

15161523
string inputTypeExpr;
@@ -1623,11 +1630,12 @@ private static void EmitGeneratedTestClass(
16231630

16241631
// Override InputShape/OutputShape for domain-appropriate test data.
16251632
// Vision/Video/3D models need [C, H, W]; default is [1, 4].
1626-
bool isVideoModel = model.Domains.Contains(3);
1633+
// Enum ordinals: General=0, Vision=1, Language=2, Audio=3, Video=4.
1634+
bool isVideoModel = model.Domains.Contains(4); // Video=4 (was incorrectly 3)
16271635
bool isFrameInterpModel = model.Tasks.Contains(35); // FrameInterpolation
16281636
bool isTemporalVideoModel = isVideoModel && !isFrameInterpModel;
16291637
bool isVisionModel = model.Domains.Contains(1) || model.Domains.Contains(11);
1630-
bool isAudioModel = model.Domains.Contains(4);
1638+
bool isAudioModel = model.Domains.Contains(3); // Audio=3 (was incorrectly 4)
16311639
if (isTemporalVideoModel)
16321640
{
16331641
// Temporal video: [frames, channels, height, width]
@@ -1660,6 +1668,23 @@ private static void EmitGeneratedTestClass(
16601668
sb.AppendLine($" protected override int[] InputShape => new[] {{ {dim} }};");
16611669
sb.AppendLine(" protected override int[] OutputShape => new[] { 4 };");
16621670
}
1671+
else if (family == TestFamily.TransformerNER || family == TestFamily.SpanBasedNER)
1672+
{
1673+
// TransformerNERBase and SpanBasedNERBase both default to
1674+
// HiddenDimension=768 (BERT-base). Inputs are validated as
1675+
// [seqLen, 768], so the base-class default [1, 4] causes a
1676+
// hard "embedding dim mismatch" failure inside MultiHeadAttention
1677+
// before any downstream logic runs. Use a short sequence to
1678+
// keep the test fast while matching the model's expected
1679+
// embedding size. Models with non-default hidden dimensions
1680+
// (TinyBERT=312, etc.) need a manual test override.
1681+
sb.AppendLine(" protected override int[] InputShape => new[] { 8, 768 };");
1682+
}
1683+
else if (family == TestFamily.SequenceLabelingNER)
1684+
{
1685+
// LSTM-CRF family defaults to EmbeddingDimension=100.
1686+
sb.AppendLine(" protected override int[] InputShape => new[] { 8, 100 };");
1687+
}
16631688

16641689
sb.AppendLine($" protected override {returnTypeCode} {factoryMethodName}()");
16651690
sb.AppendLine(factoryBody);

src/AiDotNet.Serving/Services/AesGcmModelArtifactProtector.cs

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,67 @@ public ProtectedModelArtifact ProtectToFile(string modelName, string sourcePath,
7272
}
7373
}
7474

75+
/// <summary>
76+
/// Cross-platform-invalid filename characters. Combines the Windows
77+
/// invalid set (most restrictive: ":" + "\\" + reserved punctuation +
78+
/// control chars) with POSIX "/" and "\0". Used instead of
79+
/// <see cref="Path.GetInvalidFileNameChars"/> because that method
80+
/// returns a platform-specific set — on Linux it only contains '\0'
81+
/// and '/', so a model name like "my:model" sanitizes to "my:model"
82+
/// on Linux but "my_model" on Windows. Encrypted artifacts are
83+
/// designed to be portable, so we apply the strict Windows superset
84+
/// on every OS to guarantee the output is mountable everywhere.
85+
/// </summary>
86+
private static readonly HashSet<char> CrossPlatformInvalidFileNameChars =
87+
new(new[]
88+
{
89+
'\0', '/', '\\', ':', '*', '?', '"', '<', '>', '|',
90+
}
91+
.Concat(Enumerable.Range(1, 31).Select(i => (char)i)));
92+
93+
/// <summary>
94+
/// DOS reserved device names. Creating a file with any of these as the
95+
/// base name (with or without extension) fails on Windows with
96+
/// <c>PathTooLongException</c> / <c>IOException</c> because the kernel
97+
/// still routes them to legacy character devices. Cross-platform
98+
/// portability requires rejecting them even on POSIX hosts so an
99+
/// artifact produced on Linux can't be loaded on Windows.
100+
/// </summary>
101+
private static readonly HashSet<string> WindowsReservedFileNames =
102+
new(StringComparer.OrdinalIgnoreCase)
103+
{
104+
"CON", "PRN", "AUX", "NUL",
105+
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
106+
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
107+
};
108+
75109
private static string SanitizeFileName(string name)
76110
{
77-
var invalid = Path.GetInvalidFileNameChars();
78-
var chars = name.Select(c => invalid.Contains(c) ? '_' : c).ToArray();
79-
return new string(chars);
111+
// 1. Replace cross-platform-invalid characters.
112+
var chars = name.Select(c => CrossPlatformInvalidFileNameChars.Contains(c) ? '_' : c).ToArray();
113+
var sanitized = new string(chars);
114+
115+
// 2. Windows strips trailing dots and spaces from filenames at create-time
116+
// (so "model." silently becomes "model", but "model." on some paths fails
117+
// with PathNotFound). Trim on every platform to avoid the mismatch.
118+
sanitized = sanitized.TrimEnd(' ', '.');
119+
120+
// 3. If the base (pre-extension) is a reserved DOS device name, prefix it
121+
// so the artifact remains portable. Split on the first dot so "NUL.bin"
122+
// also gets rewritten.
123+
if (sanitized.Length == 0)
124+
{
125+
return "_";
126+
}
127+
128+
var dotIndex = sanitized.IndexOf('.');
129+
var baseName = dotIndex >= 0 ? sanitized.Substring(0, dotIndex) : sanitized;
130+
if (WindowsReservedFileNames.Contains(baseName))
131+
{
132+
sanitized = "_" + sanitized;
133+
}
134+
135+
return sanitized;
80136
}
81137
}
82138

src/ComputerVision/Weights/WeightDownloader.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Net.Http;
2+
using AiDotNet.Data;
23

34
namespace AiDotNet.ComputerVision.Weights;
45

@@ -108,8 +109,10 @@ public async Task DownloadAsync(
108109
}
109110
}
110111

111-
// Move completed download to final location
112-
File.Move(tempPath, localPath);
112+
// Move completed download to final location. Uses RobustFileOps
113+
// so a transient Windows Defender / indexer lock doesn't abort
114+
// the download (the finally block would wipe the temp file).
115+
await RobustFileOps.MoveWithRetryAsync(tempPath, localPath, cancellationToken);
113116
}
114117
finally
115118
{

src/Data/DatasetDownloader.cs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,23 +73,35 @@ public static async Task<bool> DownloadFileAsync(
7373
string tempPath = destinationPath + ".tmp";
7474
try
7575
{
76-
using var response = await SharedHttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
77-
response.EnsureSuccessStatusCode();
76+
using (var response = await SharedHttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
77+
{
78+
response.EnsureSuccessStatusCode();
7879

79-
using var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None);
80+
// Close the file stream explicitly (via nested using) before
81+
// File.Move runs. The nested scope guarantees the handle is
82+
// released; otherwise the outer `using` would dispose *after*
83+
// the move attempt.
84+
using var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None);
8085
#if NET6_0_OR_GREATER
81-
await response.Content.CopyToAsync(fileStream, cancellationToken);
86+
await response.Content.CopyToAsync(fileStream, cancellationToken);
87+
await fileStream.FlushAsync(cancellationToken);
8288
#else
83-
await response.Content.CopyToAsync(fileStream);
89+
await response.Content.CopyToAsync(fileStream);
90+
fileStream.Flush();
8491
#endif
92+
}
8593

86-
// Move temp file to final location
94+
// Move temp file to final location.
8795
if (File.Exists(destinationPath))
8896
{
8997
File.Delete(destinationPath);
9098
}
9199

92-
File.Move(tempPath, destinationPath);
100+
// Retry the rename. On Windows the freshly-written file can be
101+
// briefly locked by antivirus (Defender) or the search indexer,
102+
// which makes File.Move fail with a sharing violation even after
103+
// the writer's handle is closed. Short backoff tolerates that.
104+
await RobustFileOps.MoveWithRetryAsync(tempPath, destinationPath, cancellationToken);
93105
return true;
94106
}
95107
finally

0 commit comments

Comments
 (0)