feat: align project with OZON Track 3 official specification

- Fix critical parameters: min dimensions 10×10×2 mm (was 10×10×10)
- Fix roundness threshold: K ≥ 0.7 (was 0.8)
- Add input_info analysis document (INPUT_INFO_ANALYSIS.md)
- Update Hero: new title, badges (Min 10×10×2, K≥0.7→D, C priority), proof-line
- Add CurrentProofCard summary block: Category/Command/Target/Why (cyan border)
- Add CPriorityExplanation component (orange card, dedicated section)
- Update all docs: README, DEMO_SCRIPT (7-min timing), JURY_QA, SUBMISSION_CHECKLIST
- Update tests: 16/16 passed under new parameters
- Update 3D twin: dynamic DIMENSION_LIMITS display
- Update criteria cards: roundness ≥ 0.7
- Tested: build OK, Docker OK, domains OK (arhipovdan.ru, www, ai-shorts.ru)
This commit is contained in:
root
2026-07-04 13:09:49 +02:00
parent e0a655db4f
commit 17c37c2580
17 changed files with 709 additions and 29 deletions

View File

@@ -4,6 +4,7 @@ import HeroSection from './components/HeroSection';
import ProductDemoSection from './components/ProductDemoSection';
import StorylineStepper from './components/StorylineStepper';
import ScenarioCards from './components/ScenarioCards';
import CPriorityExplanation from './components/CPriorityExplanation';
import CriteriaCards from './components/CriteriaCards';
import EngineeringDetails, { type EngineeringDetailsHandle } from './components/EngineeringDetails';
import { DEMO_STEPS } from './data/demoSteps';
@@ -79,6 +80,10 @@ export default function App() {
scrollToId('scenarios');
};
const handleShowCPriority = () => {
handleScenarioChange('c_priority');
};
return (
<div className="product-page">
<Header
@@ -113,6 +118,8 @@ export default function App() {
onScenarioChange={handleScenarioChange}
/>
<CPriorityExplanation onShowCPriority={handleShowCPriority} />
<CriteriaCards onLinkedScenario={handleScenarioChange} />
<EngineeringDetails

View File

@@ -0,0 +1,26 @@
interface CPriorityExplanationProps {
onShowCPriority: () => void;
}
export default function CPriorityExplanation({ onShowCPriority }: CPriorityExplanationProps) {
return (
<section className="c-priority-section" aria-labelledby="c-priority-title">
<div className="c-priority-card">
<div className="c-priority-icon" aria-hidden="true">
C
</div>
<div className="c-priority-content">
<h3 id="c-priority-title">Почему C имеет приоритет?</h3>
<p>
Если товар одновременно негабаритный и имеет круг в сечении, он направляется в C.
Сначала проверяются габариты, и только если они проходят — форма.
Это предотвращает попадание негабаритных объектов в основной сортировщик.
</p>
<button type="button" className="btn-primary" onClick={onShowCPriority}>
Показать C-priority
</button>
</div>
</div>
</section>
);
}

View File

@@ -29,7 +29,7 @@ const PRODUCT_CRITERIA: CriteriaCard[] = [
{
id: 'round-section',
title: 'Круг в сечении',
evidence: 'Roundness ≥ 0.8 направляет товар в зону D.',
evidence: 'Roundness ≥ 0.7 направляет товар в зону D.',
status: 'covered',
linkedScenario: 'round_object',
linkedLabel: 'Круглый объект',

View File

@@ -40,6 +40,33 @@ export default function CurrentProofCard({ simulation }: { simulation: Simulatio
<h3>{item?.name}</h3>
</div>
<div className="proof-summary">
<div className="summary-row">
<span className="summary-label">Category:</span>
<strong className={`category-${result?.category ?? 'default'}`}>{result?.category ?? '—'}</strong>
</div>
<div className="summary-row">
<span className="summary-label">Command:</span>
<strong>{command}</strong>
</div>
<div className="summary-row">
<span className="summary-label">Target:</span>
<strong>{result?.category ? `Zone ${result.category}` : '—'}</strong>
</div>
<div className="summary-row">
<span className="summary-label">Why:</span>
<em>
{isCPriority
? `dimensions failed, so C has priority even though K = ${item?.roundness.toFixed(2)}`
: result?.dimensionsPass === false
? 'dimensions failed (oversized or undersized)'
: result?.roundnessPass === false
? `dimensions pass, but K = ${item?.roundness.toFixed(2)} ≥ 0.7`
: 'dimensions pass and round section is not detected'}
</em>
</div>
</div>
<div className="proof-grid">
<div className="proof-item">
<span className="proof-label">Dimensions</span>
@@ -55,7 +82,7 @@ export default function CurrentProofCard({ simulation }: { simulation: Simulatio
<strong className={result?.roundnessPass === false && result?.dimensionsPass ? 'fail-text' : 'pass-text'}>
{item?.roundness.toFixed(2)}
</strong>
<em>{result?.roundnessPass ? 'PASS' : 'DETECTED (≥0.8)'}</em>
<em>{result?.roundnessPass ? 'PASS' : 'DETECTED (≥0.7)'}</em>
</div>
<div className="proof-item">
<span className="proof-label">Category</span>

View File

@@ -14,19 +14,18 @@ export default function HeroSection({
<div className="hero-content">
<p className="hero-eyebrow">OZON Tech · Product Demo</p>
<h1 id="hero-title" className="hero-title">
Интеллектуальная система сортировки товаров
Интеллектуальная система предварительной сортировки товаров
</h1>
<p className="hero-subtitle">
Симуляция показывает полный цикл: обнаружение товара, классификация по правилам,
управляющая команда и маршрутизация в зоны B/C/D.
3D-симуляция ПАК показывает полный цикл: товар поступает из A, определяется CV-модулем, классифицируется в B/C/D и физически маршрутизируется исполнительной частью.
</p>
<div className="hero-badges" aria-label="Ключевые возможности">
<span className="hero-badge">Computer Vision</span>
<span className="hero-badge">Rule-based classification</span>
<span className="hero-badge">Actuator routing</span>
<span className="hero-badge">Cycle time</span>
<span className="hero-badge">Fault handling</span>
<div className="hero-badges" aria-label="Параметры системы">
<span className="hero-badge">Конвейер 1 м/с</span>
<span className="hero-badge">Min 10×10×2 мм</span>
<span className="hero-badge">Max 450×320×320 мм</span>
<span className="hero-badge">K ≥ 0.7 → D</span>
<span className="hero-badge">C priority</span>
</div>
<div className="hero-actions">
@@ -65,7 +64,7 @@ export default function HeroSection({
</div>
</div>
<p className="hero-chain-caption">
Результат демо: категория B/C/D, команда ROUTE_TO_* и целевая зона
Detection → Classification → ROUTE_TO_* → Actuator → B/C/D
</p>
</div>
</section>

View File

@@ -11,6 +11,7 @@ import RouteArrows3D from './RouteArrows3D';
import SceneLabels3D from './SceneLabels3D';
import { PHYSICS_ENGINE_ENABLED } from './itemMotion';
import { NOMINAL_CONVEYOR_SPEED_MPS } from '../../domain/simulation';
import { DIMENSION_LIMITS } from '../../domain/classifier';
export interface SorterDigitalTwinProps {
simulation: SimulationState;
@@ -142,7 +143,9 @@ export default function SorterDigitalTwin({
{category ? <span className={`twin-chip category-${category}`}>Zone {category}</span> : null}
<span className="twin-chip">{command}</span>
<span className="twin-chip">Conveyor {NOMINAL_CONVEYOR_SPEED_MPS.toFixed(2)} m/s</span>
<span className="twin-chip">Min 10×10×10 mm</span>
<span className="twin-chip">
Min {DIMENSION_LIMITS.min.width}×{DIMENSION_LIMITS.min.depth}×{DIMENSION_LIMITS.min.height} mm
</span>
{showFps ? <span className="twin-chip">FPS ~{fps}</span> : null}
<span className="twin-chip muted-chip">
Motion: {PHYSICS_ENGINE_ENABLED ? 'physics' : 'state-machine'}

View File

@@ -43,7 +43,7 @@ export const DEMO_STEPS: DemoStep[] = [
scenarioId: 'round_object',
preferredAction: 'step',
focusArea: 'scene',
explanation: 'Roundness >= 0.8 after valid dimensions sends the item to D for repack/shape issue handling.',
explanation: 'Roundness >= 0.7 after valid dimensions sends the item to D for repack/shape issue handling.',
whatToWatch: 'Roundness FAIL for B, category D and the upper route/pusher D command.',
juryValue: 'Shows how the circle-section rule becomes a physical route.',
relatedCriteria: ['classification-rules', 'geometry-variety', 'physical-routing'],

View File

@@ -23,7 +23,7 @@ export const SCENARIOS: Scenario[] = [
{
id: 'round_object',
name: 'Round object',
description: 'Габариты проходят, но roundness >= 0.8, маршрут в D.',
description: 'Габариты проходят, но roundness >= 0.7, маршрут в D.',
goal: 'Показать проверку круглого сечения после габаритов.',
expectedCategorySummary: 'D for every item',
demonstrates: 'Rule-based shape issue routing без реального ML на MVP-этапе.',

View File

@@ -6,10 +6,10 @@ import type { Category, Item } from './types';
const VALID_CATEGORIES: Category[] = ['B', 'C', 'D'];
describe('classifyItem', () => {
it('uses min dimensions 10×10×10 mm', () => {
expect(DIMENSION_LIMITS.min).toEqual({ width: 10, depth: 10, height: 10 });
it('uses min dimensions 10×10×2 mm', () => {
expect(DIMENSION_LIMITS.min).toEqual({ width: 10, depth: 10, height: 2 });
expect(DIMENSION_LIMITS.max).toEqual({ width: 450, depth: 320, height: 320 });
expect(DIMENSION_LIMITS.roundnessThreshold).toBe(0.8);
expect(DIMENSION_LIMITS.roundnessThreshold).toBe(0.7);
});
it('routes normal box to B', () => {
@@ -40,11 +40,11 @@ describe('classifyItem', () => {
expect(classifyItem(getItem('SKU-010')).category).toBe('B');
});
it('rejects height below 10 mm as C', () => {
it('rejects height below 2 mm as C', () => {
const item: Item = {
...getItem('SKU-001'),
id: 'TEST-LOW-HEIGHT',
dimensionsMm: { width: 100, depth: 100, height: 9 },
dimensionsMm: { width: 100, depth: 100, height: 1 },
};
expect(classifyItem(item).category).toBe('C');
expect(classifyItem(item).dimensionsPass).toBe(false);

View File

@@ -16,9 +16,9 @@ export const CATEGORY_DEFINITIONS: Record<Category, CategoryDefinition> = {
};
export const DIMENSION_LIMITS = {
min: { width: 10, depth: 10, height: 10 },
min: { width: 10, depth: 10, height: 2 },
max: { width: 450, depth: 320, height: 320 },
roundnessThreshold: 0.8,
roundnessThreshold: 0.7,
};
export function classifyItem(item: Item): ClassificationResult {

View File

@@ -690,6 +690,46 @@ button:disabled {
font-size: 1.25rem;
}
.proof-summary {
display: flex;
flex-direction: column;
gap: 10px;
padding: 16px;
border: 2px solid var(--cyan);
border-radius: 12px;
background: rgba(56, 189, 248, 0.06);
}
.summary-row {
display: flex;
align-items: baseline;
gap: 8px;
flex-wrap: wrap;
}
.summary-label {
color: var(--cyan);
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
min-width: 88px;
}
.summary-row strong {
font-size: 18px;
font-weight: 800;
}
.summary-row em {
flex: 1;
min-width: 200px;
color: var(--muted);
font-size: 14px;
font-style: normal;
line-height: 1.4;
}
.proof-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -976,6 +1016,71 @@ button:disabled {
font-weight: 700;
}
/* ---------- C-priority explanation ---------- */
.c-priority-section {
margin: clamp(40px, 6vw, 72px) 0;
}
.c-priority-card {
display: flex;
gap: 20px;
align-items: center;
padding: clamp(20px, 4vw, 32px);
border: 2px solid var(--orange);
border-radius: 20px;
background: linear-gradient(135deg, rgba(245, 158, 11, 0.08) 0%, rgba(245, 158, 11, 0.02) 100%);
}
.c-priority-icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 72px;
height: 72px;
border-radius: 16px;
background: var(--orange);
color: #041018;
font-size: 36px;
font-weight: 900;
}
.c-priority-content {
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
}
.c-priority-content h3 {
font-size: 1.5rem;
color: var(--text);
}
.c-priority-content p {
color: var(--muted);
font-size: 15px;
line-height: 1.5;
}
.c-priority-content .btn-primary {
align-self: flex-start;
margin-top: 4px;
}
@media (max-width: 640px) {
.c-priority-card {
flex-direction: column;
text-align: center;
}
.c-priority-content .btn-primary {
align-self: stretch;
width: 100%;
}
}
/* ---------- Engineering details ---------- */
.engineering-toggle {