diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1f178df..2d40e8b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,7 @@ jobs: id: manifest env: UPDATE_BASE_URL: ${{ vars.DASHCAM_OFFLOADER_UPDATE_BASE_URL || 'https://dashcam-offloader-updates.vortexradar.workers.dev' }} - VERSION: 0.1.28 + VERSION: 0.1.29 ZIP_NAME: ${{ steps.package.outputs.zip_name }} ZIP_SHA256: ${{ steps.package.outputs.sha256 }} SHORT_SHA: ${{ steps.package.outputs.short_sha }} diff --git a/Sources/DashcamOffloaderApp/CardScannerParkingInference.swift b/Sources/DashcamOffloaderApp/CardScannerParkingInference.swift index 3f79b8e..2de1a70 100644 --- a/Sources/DashcamOffloaderApp/CardScannerParkingInference.swift +++ b/Sources/DashcamOffloaderApp/CardScannerParkingInference.swift @@ -41,6 +41,13 @@ extension CardScanner { if blackVueParkingPaths.contains(clip.relativePath) { continue } + // On T330W the owner's RO/PF clips included time-lapse parking. + // RO + parking suffix alone cannot establish an impact event. + if profileID == "viofo-t330w" && + relativeFolderPath(for: clip.relativePath).lowercased() == "dcim/movie/ro" && + hasParkingChannelSuffix(clip.filename) { + continue + } if let explicitPattern = explicitParkingPattern(for: clip) { inferredByRelativePath[clip.relativePath] = explicitPattern } diff --git a/Sources/DashcamOffloaderApp/KnownDashcamCatalog.swift b/Sources/DashcamOffloaderApp/KnownDashcamCatalog.swift index 7bd694e..9cd340b 100644 --- a/Sources/DashcamOffloaderApp/KnownDashcamCatalog.swift +++ b/Sources/DashcamOffloaderApp/KnownDashcamCatalog.swift @@ -78,6 +78,8 @@ enum KnownDashcamCatalog { model("Vueroid", "D10-F2W", channels: 2, roles: ["front", "rear"]), // VIOFO + model("VIOFO", "T330W", channels: 3, roles: ["front", "rear", "interior"], notes: "T330 family with a different rear-camera variant. Exact T330W model stamp confirmed in owner-provided video frames; shared filenames alone are not an exact identifier."), + model("VIOFO", "T330", channels: 3, roles: ["front", "rear", "interior"], notes: "T330 family; the connected rear camera distinguishes T330W. Exact non-W OSD wording has not been sampled."), model("VIOFO", "A329S", channels: 3, roles: ["front", "rear", "interior"], notes: "Multiplex video capable."), model("VIOFO", "A329T", channels: 3, roles: ["front", "rear", "telephoto"], notes: "Telephoto channel token T observed."), model("VIOFO", "A229 Pro", channels: 3, roles: ["front", "rear", "interior"]), diff --git a/Sources/DashcamOffloaderApp/VerificationTest.swift b/Sources/DashcamOffloaderApp/VerificationTest.swift index c52cfb1..e55d23c 100644 --- a/Sources/DashcamOffloaderApp/VerificationTest.swift +++ b/Sources/DashcamOffloaderApp/VerificationTest.swift @@ -688,6 +688,53 @@ enum VerificationTest { try FileManager.default.createDirectory(at: emptySource, withIntermediateDirectories: true) let scanner = CardScanner() + guard let t330WProfile = profiles.first(where: { $0.id == "viofo-t330w" }), + t330WProfile.osdSpec?.matchStrings == ["VIOFO T330W"], + KnownDashcamCatalog.models.contains(where: { $0.manufacturer == "VIOFO" && $0.model == "T330W" }) else { + print("VERIFY FAIL: T330W exact-stamp profile or catalog identity missing") + return false + } + let t330WSource = temp.appendingPathComponent("Unlabeled-T330W-fixture", isDirectory: true) + for folder in ["DCIM/Movie", "DCIM/Movie/Parking", "DCIM/Movie/RO", "DCIM/Photo"] { + try FileManager.default.createDirectory(at: t330WSource.appendingPathComponent(folder), withIntermediateDirectories: true) + } + let t330WNames = ["2026_1010_152138_000028F.MP4", "2026_1010_152138_000029I.MP4", "2026_1010_152138_000030R.MP4"] + for name in t330WNames { + try Data([1]).write(to: t330WSource.appendingPathComponent("DCIM/Movie/\(name)")) + } + for name in ["2026_1010_155138_000031PF.MP4", "2026_1010_155138_000032PI.MP4", "2026_1010_155138_000033PR.MP4"] { + try Data([1]).write(to: t330WSource.appendingPathComponent("DCIM/Movie/RO/\(name)")) + } + let t330WithoutStamp = try scanner.scan(sourceURL: t330WSource, profiles: profiles) + guard t330WithoutStamp.selectedProfile?.id == DashcamProfile.genericNewDashcam.id, + !t330WithoutStamp.clips.contains(where: { $0.channel == "unknown" }) else { + print("VERIFY FAIL: T330W shared VIOFO layout was falsely exact-identified or lost channels") + return false + } + if let ownerFrameVideo = ProcessInfo.processInfo.environment["DASHCAM_T330W_VIDEO_FIXTURE"] { + let fixtureURL = URL(fileURLWithPath: ownerFrameVideo) + let frontURL = t330WSource.appendingPathComponent("DCIM/Movie/\(t330WNames[0])") + try FileManager.default.removeItem(at: frontURL) + try FileManager.default.copyItem(at: fixtureURL, to: frontURL) + let t330WithStamp = try scanner.scanWithOSD(sourceURL: t330WSource, profiles: profiles) + guard t330WithStamp.selectedProfile?.id == t330WProfile.id, + t330WithStamp.clips.contains(where: { $0.channel == "front" }), + t330WithStamp.clips.filter({ $0.relativePath.contains("/RO/") }).allSatisfy({ $0.mode != "parking_impact_detection" }), + t330WithStamp.diagnostics.contains(where: { $0.stage == "osd_ocr_probe" && $0.profileID == t330WProfile.id && $0.outcome == "matched" }) else { + print("VERIFY FAIL: T330W stamp OCR, channels or neutral RO parking classification") + return false + } + if let noStampVideo = ProcessInfo.processInfo.environment["DASHCAM_T330W_NO_STAMP_VIDEO_FIXTURE"] { + try FileManager.default.removeItem(at: frontURL) + try FileManager.default.copyItem(at: URL(fileURLWithPath: noStampVideo), to: frontURL) + let noStampScan = try scanner.scanWithOSD(sourceURL: t330WSource, profiles: profiles) + guard noStampScan.selectedProfile?.id == DashcamProfile.genericNewDashcam.id, + !noStampScan.diagnostics.contains(where: { $0.stage == "osd_ocr_probe" && $0.profileID == t330WProfile.id && $0.outcome == "matched" }) else { + print("VERIFY FAIL: card without a model stamp falsely matched T330W") + return false + } + } + } guard let arc700Profile = profiles.first(where: { $0.id == "thinkware-arc-700" }), let arc900Profile = profiles.first(where: { $0.id == "thinkware-arc-900" }) else { print("VERIFY FAIL: ARC 700/900 manual-backed seed profiles did not load") diff --git a/docs/card-profiles/viofo-t330w.md b/docs/card-profiles/viofo-t330w.md new file mode 100644 index 0000000..4ef7e1c --- /dev/null +++ b/docs/card-profiles/viofo-t330w.md @@ -0,0 +1,17 @@ +# VIOFO T330W (T330-family rear-camera variant) + +Status: experimental, owner-submitted card samples and three owner-provided frame observations. Firmware reported by owner: `1.0_260910`. + +## Exact model recognition + +The owner-provided front, rear, and interior frames show the **`VIOFO T330W`** on-screen model stamp near the bottom center. The model stamp is user-configurable and may be disabled. Probe front `F` footage with bottom-strip OCR; only an exact model-stamp match should disambiguate this camera from VIOFO A229/A329 siblings. A generic VIOFO card layout or filename suffix is **not** proof of T330W. + +T330 and T330W share the same main-camera family; the attached rear camera is a variant. The non-W model's exact OSD stamp has not yet been confirmed. Do not assume a W stamp identifies a different main platform or assume identical measured rear bitrate without testing. + +## Card evidence + +Two sanitized learning submissions from one owner confirmed `DCIM/Movie`, `DCIM/Movie/Parking`, `DCIM/Movie/RO`, and `DCIM/Photo` with three-channel F/I/R and PF/PI/PR suffixes. The first completed scan (84 files) was owner-labeled normal-bitrate driving and low-bitrate parking. The second completed scan (36 files) was owner-labeled high-bitrate driving and 1-fps Night Vision time-lapse parking. These settings can coexist on a card; sample clips by folder/channel and do not extrapolate a setting to all files. + +The second submission's `RO` parking clips have 30-fps playback, with two recording starts 30 minutes apart and one 60-second clip per channel; this is consistent with 1-fps capture over 30 minutes. The owner reports no audio on those clips, but the sanitized scan does not include audio-track presence. Do **not** label all RO clips as impact events or all RO clips as time lapse: use model-specific media timing/audio context or a confirmed setting. The profile folder mapping stays protected/neutral pending that classification fix. + +No raw user frames or footage are distributed in this repository. App OCR is local and sends neither frames nor OSD text in feedback. diff --git a/docs/releases/latest.md b/docs/releases/latest.md index 7eb2deb..6c7e4d1 100644 --- a/docs/releases/latest.md +++ b/docs/releases/latest.md @@ -1,7 +1,6 @@ -# Dashcam Offloader 0.1.28 +# Dashcam Offloader 0.1.29 -- Prevent a card-learning submission before a complete, nonempty file scan, or after a card rescan changes the reviewed data. -- Refuse learning submissions with unreadable video bitrate metadata rather than silently sending only folder counts. Show an actionable warning in the review window. -- Show every measured video group in the review window; bitrate ranges are samples by folder, recording mode, and camera channel, not one setting assumed for the entire card. -- Scanning remains read-only. Existing footage can be rescanned without formatting the card. -- An incomplete or unreadable learning scan must be corrected before submission; it is not reported as a successful camera test. +- Identify VIOFO T330W by the exact `VIOFO T330W` model stamp in a front-channel video frame during ambiguous VIOFO scans. +- Include an experimental T330W profile for F/I/R and PF/PI/PR channel mapping and the card's Movie, Parking, RO, and Photo folders. Shared VIOFO folder layouts alone remain insufficient for exact identification; T330W RO/PF clips no longer become impact events solely from their location. +- List T330 and T330W as a family with differing rear-camera variants; the non-W model stamp has not yet been validated. +- Scanning and OCR remain local and read-only; video frames are not uploaded with scan feedback. diff --git a/docs/supported-cameras.md b/docs/supported-cameras.md index e449e2e..5032491 100644 --- a/docs/supported-cameras.md +++ b/docs/supported-cameras.md @@ -23,6 +23,7 @@ which applies. - Botslab G980H - profile drafted from an app learning submission and real 4CH card sample. Detected from `MISC/G980HMCN5291.TXT`; unknown `360CARDVR` cards should not be assigned this model without exact marker evidence. - Vueroid S1 4K Infinite - base model profile with 1CH, 2CH, and 3CH variants. One real 3CH sample card inspected. - Vueroid S1 QHD Infinite - exact config-metadata detection with 1CH, 2CH, and 3CH variants learned from four app submissions; firmware 1.0.4 30 fps 3CH, 60 fps front-only, 5 fps time-lapse parking, and 30 fps parking-impact behavior observed. +- VIOFO T330W - experimental 3CH profile from two app learning submissions and three owner-provided video frames showing the exact `VIOFO T330W` bottom-center stamp. The shared VIOFO folder/filename layout does not identify the model by itself; local OCR can identify it when the configurable stamp is enabled. T330 is the same main-camera family with a different rear-camera variant, but the non-W stamp has not been validated. See `docs/card-profiles/viofo-t330w.md`. - VIOFO A329S - base model profile. One real 3CH sample card inspected. - VIOFO A329T - related telephoto model profile drafted from official VIOFO product references and user-provided filename evidence. No real card inspected yet. - BlackVue Elite 9 - base model profile. One real 2CH sample card inspected. diff --git a/profiles/viofo-t330w.yaml b/profiles/viofo-t330w.yaml new file mode 100644 index 0000000..6260a16 --- /dev/null +++ b/profiles/viofo-t330w.yaml @@ -0,0 +1,89 @@ +id: viofo-t330w +manufacturer: VIOFO +model: T330W +status: experimental +source_notes: + - docs/card-profiles/viofo-t330w.md +detection: + high_confidence: + - method: osd_ocr + source: front_channel_video + strip: bottom_10pct + matches: "VIOFO T330W" + notes: > + Owner-provided frames show the exact model stamp near the bottom center + of front, rear, and interior footage. Do not infer this model from the + shared VIOFO card layout when the camera model stamp is disabled. + supporting: + - folder: DCIM/Movie + - folder: DCIM/Movie/Parking + - folder: DCIM/Movie/RO + - folder: DCIM/Photo +folders: + - path: DCIM/Movie + mode: continuous + validation: app_submission_sampled + - path: DCIM/Movie/Parking + mode: parking + validation: app_submission_sampled + - path: DCIM/Movie/RO + mode: protected + validation: app_submission_sampled + notes: > + May contain time-lapse parking clips, as confirmed for the owner's + 1-fps Night Vision test; do not infer impact detection from RO alone. + - path: DCIM/Photo + mode: still_image + validation: app_submission_sampled +filename_patterns: + - pattern: "^(\\d{4})_(\\d{4})_(\\d{6})_(\\d+)(F|I|R|PF|PI|PR)\\.MP4$" + applies_to: + - DCIM/Movie + - DCIM/Movie/Parking + - DCIM/Movie/RO + case_sensitive: true + timestamp: + year_group: 1 + month_day_group: 2 + time_group: 3 + format: YYYY_MMDD_HHMMSS + sequence_group: 4 + channel_group: 5 + - pattern: "^(\\d{4})_(\\d{4})_(\\d{6})_(\\d+)(F|I|R|PF|PI|PR)\\.JPG$" + applies_to: + - DCIM/Photo + case_sensitive: true + timestamp: + year_group: 1 + month_day_group: 2 + time_group: 3 + format: YYYY_MMDD_HHMMSS +channels: + F: front + I: interior + R: rear + PF: parking_front + PI: parking_interior + PR: parking_rear +grouping: + keys: + - year + - month_day + - time + expected_sets: + normal: + - F + - I + - R + parking: + - PF + - PI + - PR +exclude: + - .Trashes/** + - .fseventsd/** + - .Spotlight-V100/** + - "._*" + - format.txt + - DCIM/.diskdb + - DCIM/Movie/.dashcamexport/** diff --git a/scripts/build-macos-app.sh b/scripts/build-macos-app.sh index 2dab77f..f75e8ff 100755 --- a/scripts/build-macos-app.sh +++ b/scripts/build-macos-app.sh @@ -53,9 +53,9 @@ cat > "$CONTENTS_DIR/Info.plist" <CFBundlePackageType APPL CFBundleShortVersionString - 0.1.28 + 0.1.29 CFBundleVersion - 28 + 29 DashcamOffloaderBuildCommit $BUILD_COMMIT LSMinimumSystemVersion