Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,37 @@ jobs:
if-no-files-found: error
retention-days: 30

build-windows-store:
name: Windows Store package
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: ./.github/actions/setup

- name: Cache caption assets
uses: actions/cache@v4
with:
path: caption-assets
key: caption-assets-${{ runner.os }}-${{ hashFiles('scripts/fetch-caption-model.mjs') }}

# ponytail: STT is handled by the bundled whisper-stt-server (whisper.cpp
# with native DTW token timestamps). No separate VAD model is fetched
# here. See docs/engineering/stt-spec.md for the full architecture.

- name: Build Windows Store package
run: npm run build:win:store -- --publish never

- name: Upload Windows Store package
uses: actions/upload-artifact@v4
with:
name: openscreen-windows-store
path: release/**/Openscreen.Setup.*.appx
if-no-files-found: error
retention-days: 30

build-macos:
name: macOS ${{ matrix.arch }} DMG
runs-on: macos-latest
Expand Down
60 changes: 38 additions & 22 deletions .github/workflows/prerelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,34 +75,43 @@ jobs:
NEXT: ${{ steps.version.outputs.next }}
run: node .github/scripts/release-milestone-migrate.mjs

- name: Bump package.json to pre-release version
env:
PRERELEASE: ${{ steps.version.outputs.prerelease }}
run: |
set -euo pipefail
sed -i -E "s|(\"version\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")|\1${PRERELEASE}\2|" package.json
echo "package.json version:"
grep '"version"' package.json

- name: Commit package.json bump on a release branch
- name: Create or reuse the release branch and bump package.json
env:
TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }}
PRERELEASE: ${{ steps.version.outputs.prerelease }}
NEXT: ${{ steps.version.outputs.next }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# The release branch is FROZEN between RC cut and stable promotion. The bump
# commit lives on release/v${PRERELEASE} only; nothing is merged into main
# until promote.yml publishes the stable tag, so any features merged into
# main after this step are NOT in the RC build.
BRANCH="release/v${PRERELEASE}"
# Delete remote branch first so the push below is always fast-forward (idempotent on rerun).
git push "https://x-access-token:${TOKEN}@github.com/${GITHUB_REPOSITORY}.git" ":${BRANCH}" 2>/dev/null || true
git checkout -b "$BRANCH"
# ONE release branch per STABLE version (release/vX.Y.Z), created at rc.1 and
# FROZEN until promote publishes the stable tag. Later RCs (rc.2, rc.3, ...) are
# cut from this same branch, so they carry the rc.1 snapshot plus any
# cherry-picked bugfixes and NOT whatever has landed on main since.
#
# The name must stay in sync with promote.yml, which resolves
# release/v${STABLE_VERSION}. Naming this branch release/v${PRERELEASE}
# (with the -rc.N suffix) breaks promote and makes every re-cut a fresh
# branch off main, which silently defeats the freeze.
BRANCH="release/v${NEXT}"
REMOTE="https://x-access-token:${TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
# Re-cut (rc.2+): build on the frozen branch. Never delete it — it carries the
# cherry-picks that make this RC differ from the previous one.
echo "Reusing frozen release branch ${BRANCH}"
git fetch origin "$BRANCH"
git checkout -B "$BRANCH" "origin/${BRANCH}"
else
# First cut (rc.1): branch from the dispatched ref (main).
echo "Creating release branch ${BRANCH} from ${GITHUB_REF_NAME}"
git checkout -b "$BRANCH"
fi
sed -i -E "s|(\"version\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")|\1${PRERELEASE}\2|" package.json
echo "package.json version:"
grep '"version"' package.json
git add package.json
git commit -m "chore(release): bump to ${PRERELEASE} [skip ci]"
git push "https://x-access-token:${TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$BRANCH"
git commit -m "chore(release): bump to ${PRERELEASE} [skip ci]" || echo "(version already at ${PRERELEASE})"
git push "$REMOTE" "$BRANCH"

- name: Push RC tag on the release branch
env:
Expand All @@ -111,10 +120,10 @@ jobs:
# Note: GITHUB_TOKEN tag pushes do NOT trigger build.yml in this org's setup,
# so we explicitly trigger it via gh workflow run right after.
RC_TAG: ${{ steps.version.outputs.rc_tag }}
PRERELEASE: ${{ steps.version.outputs.prerelease }}
NEXT: ${{ steps.version.outputs.next }}
run: |
set -euo pipefail
BRANCH="release/v${PRERELEASE}"
BRANCH="release/v${NEXT}"
git fetch origin "$BRANCH"
git checkout "$BRANCH"
git reset --hard "origin/${BRANCH}"
Expand All @@ -133,7 +142,14 @@ jobs:
# GITHUB_TOKEN tag pushes don't fire the build.yml trigger in this setup,
# so dispatch it explicitly. The PAT ensures the build's release creation
# propagates to Tier 3 (homebrew/winget/nix/aur) via release: published.
#
# --ref is REQUIRED: the RC version bump lives ONLY on the release branch /
# RC tag, never on the default branch. Without --ref the build runs on main
# (still the previous stable version), so build.yml's publish-release step
# fails its guard ("package.json version X does not match <rc> from tag").
# Pin the build to the RC tag so checkout gets the bumped package.json + code.
gh workflow run build.yml \
--ref "${RC_TAG}" \
-f release_tag="${RC_TAG}" \
-f arch=both \
--repo "$GITHUB_REPOSITORY"
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/promote.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,13 @@ jobs:
run: |
set -euo pipefail
# GITHUB_TOKEN tag pushes don't fire build.yml in this setup, dispatch it.
#
# --ref pins the build to the stable tag (the frozen release-branch tip). The
# prior main-merge step usually leaves main at the stable version already, but
# relying on that is fragile; building the tag guarantees checkout has the
# matching package.json + code and enables signing/notarization (tag has no '-').
gh workflow run build.yml \
--ref "${STABLE_TAG}" \
-f release_tag="${STABLE_TAG}" \
-f arch=both \
--repo "$GITHUB_REPOSITORY"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ playwright-report/

# Vitest browser mode screenshots
__screenshots__/
.vitest-attachments/

# shell files
/shell.sh
Expand Down
88 changes: 88 additions & 0 deletions docs/engineering/main-to-ai-edition-port.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Portage `main` → `feat/ai-edition` (juillet 2026)

## Contexte

`feat/ai-edition` diverge lourdement de `main` : **273 commits** d'un côté
(compositeur natif poc-d3d, éditeur V4, timeline clip-anchored, export v2
multi-asset), **24 commits** de l'autre depuis le point de divergence
(`68d3a685`, 15 juil. 2026). Là où `main` a corrigé l'éditeur navigateur
legacy, `ai-edition` l'a **supprimé** — donc la plupart des correctifs `main`
patchent du code mort ici.

Stratégie retenue (au lieu d'un rebase brut des 273 commits) :

1. **Cherry-pick** ce qui est archi-compatible.
2. **Portage fonctionnel + revalidation** là où le même fichier a divergé mais
reste vivant (export audio).
3. **Recensement** des features liées au code supprimé → décider, par feature,
si c'est déjà couvert en V4/natif, obsolète, ou à ré-implémenter.

Un rebase littéral aurait rejoué `ai-edition` par-dessus ces 24 commits ; le
contenu **fonctionnel** net qu'il aurait apporté est exactement les 11 commits
ci-dessous (les 13 autres = bumps de version, workflow Discord, docs RC du
legacy, et patches de l'éditeur supprimé — no-ops ou bruit).

## 1. Cherry-pické (archi-compatible) — 11 commits

| Commit | Feature | Résolution de conflit |
|---|---|---|
| `e129e408`, `49338719` | Infra release (pin dispatch, nommage branche RC) | clean (workflows non touchés par ai-edition) |
| `d75cb57e` | WGC : ne plus tenir le mutex pendant `WriteSample` (#115) | **fusion** : timing webcam horloge-réelle CFR d'ai-edition **+** split capture-sous-mutex / submit-hors-mutex de `main`. `writeBgraFrame`→`captureBgraSample`. |
| `21828bfd` | WGC : dims capture arrondies au pair | clean |
| `90d9bf2f` | WGC : garde-fou anti-hang (`cv.wait`→`wait_for(100ms)`) | clean |
| `cd088673` | HUD : le drag ne dérive plus du curseur | garde `hudAllocatedSizeRef` d'ai-edition **+** ajoute `isDraggingHudRef` |
| `c6cd9436` | Test de régression drag HUD | garde `shrink-0` d'ai-edition **+** `data-testid` |
| `42ab20dc` | Packaging Microsoft Store (MSIX/appx) | `build:win:store` reflète les étapes natives d'ai-edition (compositor + ffmpeg), sinon le build Store livrerait sans compositeur natif |
| `282a617e` | Désactiver Vulkan sur Wayland (import DMA-BUF PipeWire) | clean — ai-edition **a** le bloc Wayland ciblé |

## 2. Portage fonctionnel — mixage audio multi-piste (`1c01a873`, `f65de972`)

**Bug (macOS)** : les captures natives macOS écrivent système + micro en 2 pistes
AAC ; l'exporteur ne prenait que la 1re (souvent silencieuse) → export muet
malgré un micro enregistré.

**Pourquoi c'était le point sensible** : `audioEncoder.ts` est **vivant** sur
ai-edition (l'export v2 `documentExporter`/`renderPlan`/`audioConcatPlan`
l'appelle encore) et `audioConcatPlan` est couplé à sa comptabilité
d'échantillons.

**Ce qui a rendu l'intégration safe** : le changement d'ai-edition sur
`audioEncoder.ts` est **purement additif** (+72/-0 : une nouvelle méthode
`encodePcmToMuxer` pour la boucle segment v2) et **disjoint** du chemin de
décodage que `main` réécrit → `audioEncoder.ts` s'est appliqué **clean**. Le
mixage est réellement câblé : `streamingDecoder.loadMetadata()` renvoie
`audioStreamCount` → passé au blocker source-copy qui refuse le multi-piste →
le chemin plein mixe via `mixPlanarSources`. Pas inerte.

Conflits résolus à la main dans `videoExporter.ts` / `.test.ts` en **conservant
les deux** familles de blockers (frame-rate/codec d'ai-edition + multi-piste
`#108` de `main`), `SourceCopyVideoInfo` enrichi de `audioStreamCount?`.

**Revalidation** : `tsc` clean, **118/118 tests** (les tests de mixage de `main`
+ les 4 tests d'export v2 d'ai-edition passent ensemble), biome clean. Le test
navigateur `audioMixExport.browser.test.ts` (fixture dual-audio) n'a pas été
exécuté ici (nécessite `npm run test:browser:install`) — à lancer via
`npm run test:browser`.

## 3. Recensement des features divergentes (code legacy supprimé)

| Feature `main` | Verdict | Détail |
|---|---|---|
| `e4ef4768` perf playhead découplé du re-render ancêtre (#111) | ✅ **Déjà couvert** | `V4Timeline` a `PlayheadOverlay` memoïsé + `rafSeekRef`/`pendingSeekTimeRef` (coalescing rAF) + `playheadElRef` (DOM direct). Archi découplée équivalente/supérieure. |
| `574b685c` ref écrit en render (anti-pattern React, #120) | ✅ **Déjà couvert** | `PlayheadOverlay` V4 est pur/memoïsé, `pct` en prop, aucune écriture de ref en render. |
| `9844c782` zoom auto suit le curseur (`focusMode:"auto"`, #72) | ✅ **Déjà couvert** | `src/lib/ai-edition/store/zoomSuggestions.ts` met déjà `focusMode:"auto"` par défaut sur les zooms auto-suggérés (+ `zoomSuggestions.test.ts`). Exactement le fix #72. |
| `5cfdeb3c`/`0884032f` #2 durée capée après reload | ✅ **Déjà couvert** | ai-edition remplace la durée provisoire (placeholder 60s) par la vraie durée média au probe : `applyProbedDuration.ts`, gardes `useTimeline.ts`, commit `49602a14`. La race legacy (`resetDurationResolution` clobbered) n'existe pas dans ce modèle. |
| `5cfdeb3c`/`0884032f` #1 annotation texte vide + placeholder | ⚠️ **Décision produit** | ai-edition crée le texte avec un défaut *baked* `content:"New annotation"` (`useTimeline.ts:259`), pas vide+placeholder. Pas le bug legacy exact (« Enter text… »), mais même friction UX possible. À trancher : garder « New annotation » (défaut sensé) ou aligner sur vide+placeholder. |
| Docs RC e2e (`1e572232`, `dacea226`, `7013b8bd`) | ⛔ **Obsolète** | `rc-e2e-checklist.md` supprimé sur ai-edition ; écrit contre l'UI de l'éditeur legacy. |
| Bumps de version + workflow Discord | ⛔ **Skip** | Chore de release ; ai-edition gère son propre versioning. |

## Seule décision ouverte

Le défaut de contenu des annotations texte (⚠️ ci-dessus) — choix UX à valider.
Tout le reste des features divergentes est déjà ré-implémenté nativement dans
ai-edition.

## Repères techniques

- Ref de sécurité avant cherry-picks : tag `rebase-safety-before-cherrypick`.
- `node_modules` du worktree = junction vers le repo principal (pour tsc/vitest).
13 changes: 13 additions & 0 deletions electron-builder.json5
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,18 @@
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
// Microsoft Store packaging identity, from Partner Center's "Product identity" page
// (Applications et jeux > OpenScreen > Product identity).
"appx": {
"identityName": "EtienneLescot.OpenScreen",
"publisher": "CN=03B0FC8C-2067-45D9-BE82-9F15CE264B4A",
"publisherDisplayName": "Etienne Lescot",
"applicationId": "Openscreen",
"displayName": "OpenScreen",
"backgroundColor": "transparent",
"capabilities": ["runFullTrust", "microphone", "webcam"],
"languages": ["en-US", "fr-FR"],
"showNameOnTiles": true
}
}
6 changes: 6 additions & 0 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ if (process.platform === "linux") {
app.commandLine.appendSwitch("ozone-platform", "wayland");
// Enable WebRTCPipeWireCapturer for screen capture on Wayland
app.commandLine.appendSwitch("enable-features", "WaylandWindowDrag,WebRTCPipeWireCapturer");
// Chromium's Wayland Ozone backend can't use Vulkan. When it tries, the WebRTC
// PipeWire capturer fails to import DMA-BUF frames into EGL (EGL_BAD_MATCH), the
// stream renegotiates, and screen recording yields no usable frames. Force the
// GL/EGL path so DMA-BUF import works. (Chromium itself logs this suggestion:
// "'--ozone-platform=wayland' is not compatible with Vulkan ... disabling Vulkan".)
app.commandLine.appendSwitch("disable-features", "Vulkan");
}
}

Expand Down
Loading
Loading