زه زما د اسکرین تحلیل وسیله د یو دلیل له امله جوړ کړم: زه غواړم چې یو بریالیتوب عملی دی چې د اسکرین ریکارډ کولو په اړه وګورئ، هغه څه چې د کار فورمه په توګه ترسره کیږي، او بیا د دې درک په اتومات فابریکه کې بدل کړئ (n8n فورمه، پړاو لیستونه، جوړ شوی خلاصونه، ټول پایپ). د ګټور برخه د JSON تولید یا د راپور ورکولو نه ده. دا د multimodal درک کولو مرحله ده - هر اضافي فریم چې تاسو د يو ماډل ته ورسیږي دا حقیقي پیسې ده. د پردې ریکارډونه د پایلې نرخ نمونې لپاره بدترین حالت دی: د سټیټیک UI اوږد تارونه، بیا ناڅاپي micro-bursts چې د کاروونکي کلیک کوي، دوه ټوکرونه ټیپوي، یو dropdown خوندي، يو modal flashes، یا یو tab swaps. نو زه د فریمونو په توګه د "د معلوماتو ټکي" په توګه د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر د معیارونو په څیر. هغه څه چې لومړی غلط شوی (د ناکامۍ چې د بدلون ته اړتیا لري) زما لومړي قطع د واضح دی: د هر Nth فریم نمونې. دا نسخه په دوه لارو کې ناکام شو، او دوی په محصول کې شتون لري. په زما ډیزاین بورډ کې یو معمولي اپلوډ د 6-12 دقیقې ریکارډ دی. په ډیری دې ریکارډونو کې، د کاروونکي د فکر کولو لپاره بندوي، یو پاڼه وګورئ، یا د کورسر یوازې دلته وي. یوځای نمونې خوشحاله ده چې په دې دقیقې کې د نږدې نندارې نندارې راټولوي. د تحلیل د لګښت د ویډیو اوږدوالي سره په لړ ډول کچول کیږي حتی کله چې د معلوماتو محتویات نه دي. Failure #1: it wasted frames on dead air. د بدترین واقعې (په هغه کې چې ما د نمونې په اړه بیاکتنه وکړه) یو مختصر admin workflow ریکارډ شوی دی چې په دې کې د مهمو اجازهونو ډاونلوډونه چمتو شوي او په چټکۍ سره بند شوي. د یوځای نمونې ډاونلوډونکي د ډاونلوډ کولو مخکې او په چټکۍ سره وروسته فریمونه راټول کړ - نو د ماډل هیڅکله د اجازه انتخاب نه وګورئ. د صادراتو خلاصې ډاډمن او غلط دی: دا د مختلفې ترتیباتو بدلون پایلې، ځکه چې دا د پایلې پاڼه حالت وګورئ، مګر نه د بیلابیلو UI چې دا رامینځته کړ. Failure #2: it missed the “blink-and-you-miss-it” UI moments. دا ترکیب - د سټیک وخت لپاره پیسې ورکولو په داسې حال کې چې اوس هم د واقعي عمل په لټه کې دي - غیر قابل تحمل دی. زه اړتیا لرئ چې د نمونې کارن چې د ویډیو بدلون په ځای کې پیسې ورکوي او د سټیک UI په لټه کې پیسې ورکوي. د کلیدي مفکوره: د فریمونو پرته کولی شي چې معلومات دی. د ناڅاپي لارښوونې ده "د هر Nth فریم نمونې." دا د عادلانه احساس کوي. دا هم غلط دی. د اسکرین ریکارډ د فلم نه دی. دا د رګور ته نزدیک دی: اوږد ثبات شوي حالتونه د مختصر ترانسپورتونو لخوا پوښل کیږي. یوځای نمونې دوه بد پایلې تضمین کوي: تاسو د مستحکم UI لپاره اضافي پیسې ورکړئ. تاسو د مختصر بڼهونو کې د ناوړه پیسې ورکړئ چې د کار د جریان په حقیقت کې ترسره کیږي. یو انالوژۍ (په یوه وخت کې زه به دا کاروئ او په بل کې چمتو شي): په مساوي ډول نمونې برابر دی لکه څنګه چې هر ټیم ته د اغېز په اړه د ورته بونس ورکړئ. دا ساده حساب ورکړئ، نه ښه تادیه. د حل دی چې د بدلون په ارزانه توګه اندازه کولو، د وخت په سیسټمونو کې رامینځته کړي، او په دې سیسټمونو کې د fixed keyframe بودجه تادیه کړي. Runtime آرشیفیکټ: په کوم ځای کې د sampler تعقیب کوي د عملیاتو په اړه، د سیستم په دوو همکارۍ برخو کې تقسیم کیږي: د پیټون تجزیه خدمت (د Cloud Run خدمت په توګه تاسیس شوی) چې د اپلوډ شوي ویډیو پروسس کوي، د Keyframes انتخاب کوي، د multimodal تجزیه کولو ترسره کوي، او د جوړ شوي پایلې د ګمرک تولید کوي. د Next.js اپلیکیشن چې د webhook له لارې د پایلو ترلاسه کوي او د هغې په پایله کې کوي (او د ډیزاین بورډ UI چلوي). د نمونې کارونکي د ارزانه ماډل تماس مخکې د تحلیلر په داخله کې ژوند کوي. د غیر واضح نقطې: د نمونې په توګه نه ده د "صالحې غوره کولو". دا د کنترول سطحه ده. دا بدلون کوي "کیا دا ویډیو اوږد دی؟" ته "که څومره تحلیل زه غواړم پیسې ورکړم؟" د تحلیلر د پایلو له لارې د Next.js اپلیکیشن ته د ثبت شوي Webhook-HMAC-SHA256 له لارې د JSON بټونو په اړه، سره تصدیق کیږي په ترلاسه کولو پای کې. د مهمې درس دی: د بټونو چې تاسو په واقعیت کې پیژندل کوو ( ), نه یو پیتون ډک چې د HTTP کتابتون لخوا re-serialized کیږي. دا نښلیدو د تفتیش ناکامۍ تولید کوي چې د روحونو په څیر احساس کوي. مګر د webhook نښلیدو یو بل پست دی - هغه څه چې مهم دی دلته دا ده چې څه شي د payload د analyzer له لاسه ورکوي. timingSafeEqual data=body_bytes مخکې Adaptive keyframe sampling: score → segment → allocate → pick انډیز د نمونې پروژې د 4 مرحله پایپ دی: د ګمرک بدلون په ارزانه توګه په هر فریم کې (یا د فریمونو لپاره) بدل کیږي. د وخت لړۍ په "د عموما مستحکم" او "د لوړ بدلون" چلندونو کې راټول کړئ. د guardrails سره په ټولګیانو کې د fixed keyframe budgets تادیه کړئ. د هر برخې کې د کارتنونو په شاوخوا کې انتخاب کړئ. دا جوړښت هغه څه دی چې دا عملی کوي. د امتیاز ارزانه دی. د segmentation linear دی. د توزیع د مخکښ دی. د استخراج مرحله ميخانيکي دی. مرحله 1 - د پايلې: د ګرځنده بصری بدلون زه په ارزانه کچول کولو په اړه د اغیزمنۍ په اړه دي. که کچول کولو ډیری لګښتونه لري، زه یوازې په پایپ کې د فاکس څخه مخنیوي. د اسکرین ریکارډونو لپاره تر ټولو قابل اعتماد بیلګې سیگنال دی : frame difference energy په grayscale بدل کړئ. د دوامداره فریمونو تر منځ د بشپړ فرق محاسبه کړئ. د diff انځور (د اختیاري normalizing) منځني راټول کړئ. دا پوښونه: د Cursor حرکت د ټیپ کولو (Blinking caret او Text updates) Dropdowns او ماډلونه د مخابراتو Hover دولت بدلونونه دا بشپړ نه ده، مګر دا چټک دی او په ښه توګه د "کڅوړه واقعه" سره تړاو لري. مرحله 2 - د سیګن جوړولو: د صداقت ریکارډ په جریان کې بدل کړئ Per-frame ټیټونه پیاوړي دي. زه نه غواړم چې د توزیعونکي د غږونو په لټه کې وي. نو زه د hysteresis سره یو ساده حالت ماشین په کارولو سره segment: د روښانه او متوسط ټیټ پای ته ورسيږي. د "کړتیا" برخه کې بدلون کله چې د رول کولو ټیټ د ټیټ کچه څخه زیات وي. د "کړتیا" ته راځي کله چې دا د ټیټ ټیټ کمېسيون څخه نیسي. د ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ ټیټ دا د انجنيرۍ نه ده. دا انجنيرۍ: ثابته رفتار، د کم ټونینګ اوورفټ، او مخکښ محصول. مرحله 3 - د سپارلو سره د بودجه تادیه خالص نسبتا توزیع نه کافی دی. دا په حلقوي کې ناکام کیږي او دا کولی شي د مختصر قطعاتو په خوځښت کې وي. نو زما د تادیاتو لپاره درې قواعد لري: د هر سیګنټ یو طبقه لري (min_frames_per_segment). هیڅ سیګرام کولی شي د کټ څخه زیات وي (max_frames_per_segment). د بډایه بډایه به په برابره توګه د سټیټ سټیټ کې تقسیم شي. دا د توزیع ثابته کوي او د ناروغۍ مخنیوی کوي. مرحله 4 - په برخو کې د انډیز انتخاب کله چې یو سیګرام د K فریمونو ته ورسیږي، زه د سیګرام په پراخه کچه د K انډیزونو انتخاب وکړم: په هر وخت کې د سیسټم د پیل په شمول شي (تغیرونه موضوع). په هر وخت کې د segment end (final state matters) شامل کړئ. د نږدې بڼې سره په مساوي ډول سپارښتنه وکړئ. که زه وروسته ډیر وفادارتیا ته اړتيا لري، زه کولی شي د ټیکنالوژۍ محلي اعظمي ته پیژندل شي، مګر مساوي سپارښتنه انتخاب یو قوي اساس دی او د کوډ مستقیم کوي. بشپړ چلند وړ implementation (scoring + segmentation + توزیع + استخراج) د دې پست کاپی کولو لپاره، دلته یو واحد پیټون سکرپټ دی چې تاسو کولی شئ د هر MP4 سره کار واخلئ. دا په موثریت سره د کلیدي فریمونو انتخاب کوي او دوی ته د صادراتو لیږد ته ورسوي. د تقاضا: د OpenCV-Python شمیره د چلولو: python adaptive_keyframes.py --video input.mp4 --budget 60 --out ./keyframes دلته : adaptive_keyframes.py import argparse import os from dataclasses import dataclass from typing import List, Tuple import cv2 import numpy as np @dataclass class Segment: start: int # inclusive frame index end: int # exclusive frame index score: float @property def length(self) -> int: return max(0, self.end - self.start) def frame_diff_score(prev_bgr: np.ndarray, curr_bgr: np.ndarray) -> float: """Cheap per-frame change score in [0, 1] (roughly). Uses grayscale mean absolute difference normalized by 255. """ prev_gray = cv2.cvtColor(prev_bgr, cv2.COLOR_BGR2GRAY) curr_gray = cv2.cvtColor(curr_bgr, cv2.COLOR_BGR2GRAY) diff = cv2.absdiff(prev_gray, curr_gray) return float(diff.mean() / 255.0) def compute_scores( cap: cv2.VideoCapture, stride: int = 1, max_frames: int | None = None, ) -> Tuple[List[float], int]: """Return (scores, total_frames_read). scores[i] is the change score between frame i and i+stride (based on sampled reads). """ scores: List[float] = [] ok, prev = cap.read() if not ok: return scores, 0 frame_idx = 1 frames_read = 1 while True: # Skip stride-1 frames between comparisons. for _ in range(stride - 1): ok = cap.grab() if not ok: return scores, frames_read frame_idx += 1 frames_read += 1 if max_frames is not None and frames_read >= max_frames: return scores, frames_read ok, curr = cap.read() if not ok: return scores, frames_read frames_read += 1 s = frame_diff_score(prev, curr) scores.append(s) prev = curr frame_idx += 1 if max_frames is not None and frames_read >= max_frames: return scores, frames_read def segment_scores( scores: List[float], window: int = 8, hot_thresh: float = 0.030, cold_thresh: float = 0.020, min_len: int = 12, ) -> List[Segment]: """Convert per-step scores into segments with a utility score. Uses a rolling mean with hysteresis to avoid segment flicker. """ if not scores: return [] # Rolling mean via cumulative sum. x = np.array(scores, dtype=np.float32) c = np.cumsum(np.insert(x, 0, 0.0)) def roll_mean(i: int) -> float: j0 = max(0, i - window + 1) n = i - j0 + 1 return float((c[i + 1] - c[j0]) / n) segments: List[Segment] = [] state_hot = False seg_start = 0 seg_scores: List[float] = [] for i in range(len(scores)): rm = roll_mean(i) if state_hot: seg_scores.append(scores[i]) if rm < cold_thresh: # Close hot segment at i+1 seg_end = i + 1 if seg_end - seg_start < min_len: # Too short: merge into previous if possible, else keep. pass segments.append(Segment(seg_start, seg_end, float(np.mean(seg_scores) if seg_scores else 0.0))) # Start cold state_hot = False seg_start = seg_end seg_scores = [] else: if rm > hot_thresh: # Close cold segment seg_end = i + 1 cold_score = float(np.mean(scores[seg_start:seg_end]) if seg_end > seg_start else 0.0) segments.append(Segment(seg_start, seg_end, cold_score)) # Start hot state_hot = True seg_start = seg_end seg_scores = [] # Close tail tail_end = len(scores) if tail_end > seg_start: tail_score = float(np.mean(scores[seg_start:tail_end])) segments.append(Segment(seg_start, tail_end, tail_score)) # Merge very short segments to keep output stable. merged: List[Segment] = [] for seg in segments: if not merged: merged.append(seg) continue if seg.length < min_len: prev = merged[-1] combined = Segment(prev.start, seg.end, (prev.score * prev.length + seg.score * seg.length) / max(1, (prev.length + seg.length))) merged[-1] = combined else: merged.append(seg) # One more pass: ensure non-empty and strictly increasing. cleaned: List[Segment] = [] for seg in merged: if seg.length <= 0: continue if cleaned and seg.start < cleaned[-1].end: seg = Segment(cleaned[-1].end, seg.end, seg.score) if seg.length > 0: cleaned.append(seg) return cleaned def allocate_frames( segments: List[Segment], budget: int, min_frames_per_segment: int = 1, max_frames_per_segment: int = 30, ) -> List[int]: """Allocate keyframes to segments using floor + proportional + cap.""" if budget <= 0 or not segments: return [] n = len(segments) min_total = min_frames_per_segment * n # If budget is smaller than the floor, distribute 1-by-1. if min_total >= budget: alloc = [0] * n for i in range(budget): alloc[i % n] += 1 return alloc utilities = np.array([max(0.0, s.score) for s in segments], dtype=np.float64) total_u = float(utilities.sum()) alloc = [min_frames_per_segment] * n remaining = budget - min_total if total_u == 0.0: raw = np.full(n, remaining / n, dtype=np.float64) else: raw = utilities * (remaining / total_u) # Add integer parts. for i in range(n): add = int(raw[i]) alloc[i] = min(max_frames_per_segment, alloc[i] + add) allocated = sum(alloc) # Distribute leftover by fractional parts, respecting caps. if allocated < budget: frac = raw - np.floor(raw) order = np.argsort(-frac) # descending fractional idx = 0 safety = 0 while allocated < budget and safety < 10_000: i = int(order[idx % n]) if alloc[i] < max_frames_per_segment: alloc[i] += 1 allocated += 1 idx += 1 safety += 1 # If we somehow exceeded budget due to caps/floor interplay, trim from lowest utility. if allocated > budget: order = np.argsort(utilities) # ascending utility idx = 0 safety = 0 while allocated > budget and safety < 10_000: i = int(order[idx % n]) if alloc[i] > 0 and alloc[i] > min_frames_per_segment: alloc[i] -= 1 allocated -= 1 idx += 1 safety += 1 return alloc def pick_indices_for_segment(seg: Segment, k: int) -> List[int]: """Pick k indices in [seg.start, seg.end] over the score-step domain. Note: scores are defined between frames; we later map these to actual frames. """ if k <= 0 or seg.length <= 0: return [] if k == 1: return [seg.start] # Evenly spaced across [start, end-1] xs = np.linspace(seg.start, seg.end - 1, num=k) idxs = sorted({int(round(x)) for x in xs}) # Ensure exactly k by filling gaps if rounding collapsed points. while len(idxs) < k: # Insert midpoints between existing points. candidates = [] for a, b in zip(idxs, idxs[1:]): if b - a >= 2: candidates.append((a + b) // 2) if not candidates: # Fall back: walk forward. x = idxs[-1] if x + 1 < seg.end: idxs.append(x + 1) else: break else: for c in candidates: if c not in idxs and seg.start <= c < seg.end: idxs.append(c) if len(idxs) >= k: break idxs = sorted(idxs) # Trim if we overshot. return idxs[:k] def select_keyframe_indices(segments: List[Segment], alloc: List[int], stride: int = 1) -> List[int]: """Return concrete frame indices (0-based) to extract from the video.""" chosen: List[int] = [] for seg, k in zip(segments, alloc): step_idxs = pick_indices_for_segment(seg, k) # Map score-step domain to frame indices. # score i corresponds to diff between frame i and i+stride; # picking frame i is a reasonable representative. for si in step_idxs: chosen.append(si * stride) chosen = sorted(set(chosen)) return chosen def extract_frames(video_path: str, frame_indices: List[int], out_dir: str) -> None: os.makedirs(out_dir, exist_ok=True) cap = cv2.VideoCapture(video_path) if not cap.isOpened(): raise RuntimeError(f"Failed to open video: {video_path}") frame_set = set(frame_indices) max_idx = max(frame_set) if frame_set else -1 idx = 0 saved = 0 while idx <= max_idx: ok, frame = cap.read() if not ok: break if idx in frame_set: path = os.path.join(out_dir, f"frame_{idx:06d}.jpg") ok2 = cv2.imwrite(path, frame) if not ok2: raise RuntimeError(f"Failed to write: {path}") saved += 1 idx += 1 cap.release() if saved == 0 and frame_indices: raise RuntimeError("No frames were saved; check indices and video decoding") def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--video", required=True, help="Path to input video") ap.add_argument("--out", required=True, help="Output directory for keyframes") ap.add_argument("--budget", type=int, default=60, help="Total keyframes to extract") ap.add_argument("--stride", type=int, default=2, help="Compare every Nth frame for scoring") ap.add_argument("--window", type=int, default=8, help="Rolling window for segmentation") ap.add_argument("--hot", type=float, default=0.030, help="Enter hot segment threshold") ap.add_argument("--cold", type=float, default=0.020, help="Exit hot segment threshold") args = ap.parse_args() cap = cv2.VideoCapture(args.video) if not cap.isOpened(): raise RuntimeError(f"Failed to open video: {args.video}") scores, frames_read = compute_scores(cap, stride=args.stride) cap.release() segments = segment_scores(scores, window=args.window, hot_thresh=args.hot, cold_thresh=args.cold) alloc = allocate_frames(segments, budget=args.budget, min_frames_per_segment=1, max_frames_per_segment=max(2, args.budget)) keyframes = select_keyframe_indices(segments, alloc, stride=args.stride) # Keep within a hard limit (rounding/uniqueness can change count). if len(keyframes) > args.budget: keyframes = keyframes[: args.budget] extract_frames(args.video, keyframes, args.out) print(f"frames_read={frames_read}") print(f"scores={len(scores)} segments={len(segments)}") print(f"budget={args.budget} selected={len(keyframes)}") if segments: hot_share = sum(1 for s in segments if s.score > args.hot) / len(segments) print(f"segment_hot_share={hot_share:.2f}") if __name__ == "__main__": main() دا سکرین intentionally مستقیم دی: دا د مستحکم برخه لیست تولید کوي. دا تضمین کوي چې تاسو هیڅکله ستاسو د پروګرام بودجه څخه زیات نه وي. دا د Downstream تجزیه کولو لپاره د Deterministic Frame Files لیکنه کوي. په زما تولید خدمت کې، د extracted frames د multimodal call (Gemini له لارې په زما dependencies)، او د پایلې تجزیه د ګټور وزن له لارې په Next.js اپلیکیشن ته د امین شوي webhook له الرې اخلي. google-generativeai عملی ټونینګ نوټونه (د لومړي ډیمو وروسته څه مهمه ده) کله چې تاسو د شکل کار کوي، د ګټې د بدیل حقیقي نړۍ ریکارډونو لاندې تناسب رفتار څخه رامینځته کیږي. 1) غوره کړئ چې ستاسو د موادو سره مطابقت کوي که تاسو په 30 FPS ریکارډ کې هر فریم ټیټ کړئ، تاسو کولی شئ په هر ځای کې کوچني کورر حرکتونه ونیسئ. دا تل بد نه ده، مګر دا ستاسو د "غیر" سیگنال ته وده ورکړي. د 2-5 پايلې د اسکرین ریکارډونو لپاره یو ښه پیل پايلې ده. تاسو د UI بدلونونو په اړه هم حساس دی، مګر تاسو د زیربیم ریمو کم کړئ. 2) Hysteresis مخنیوی segment flicker کارول او (د دوو ټیټونه) مهمه دي. د یو واحد ټیټ سره، تاسو به د ګرمو / سردو تر منځ په دوامداره توګه د کټګوریو په اوږدو کې بدل کړئ. hot_thresh cold_thresh Hysteresis تاسو ته ثابته سیګنونه ورکوي او د بودجهونه په مخکښ توګه چلند کوي. 3) د پوښونو او پوښونو د اختیاري نه دي د ځمکې پرته، د مختصر برخو کولی شي په لټه کې شي او تاسو به د دقیق micro-burst چې تاسو اړتیا لري له لاسه ورکړئ. پرته، یو اوږد "کامل" برخه ستاسو د بشپړ بودجه مصرف کولی شي او تاسو به د کار د جریان د بډایه څخه اړیکه ونیسئ. 4) په هر وخت کې هغه بائټونو کې چې تاسو یې ورکړئ که تاسو د ولډنګ ، د HTTP کتابتون کولی شي سره د مختلفو کلیدونو ترتیب / spacing سره serialized شي چې تاسو په نامه ونیسئ. دا د اغیزمن تصدیق ناکامۍ تولید کوي چې احساس کوي چې د روحونو په څیر. json.dumps(payload) json=payload د واقعي بټونو د امانت کولو ( ) د بیګونو ټول ټولګي د مخنیوي. دا دلته مهمه ده ځکه چې د تجزیې ګټه ورکشاپ - هغه چې ستاسو په دقت سره انتخاب شوي کلیدي فریمونه لري - د پایپینګ کې ترټولو ارزانه آرټاکټ دی. که دا له خوا د امانت کولو غلطی له امله وټاکل کیږي، تاسو د بشپړ فریم بودجه ضایع کړئ. data=body_bytes چرا دا ډیزاین په تولید کې کچه د نمونې کار کوي ځکه چې دا د دوو سخت محدودیتونو ته احترام کوي: د لګښت کچې سره انتخاب شوي فریمونه، نه د ویډیو اوږدوالی. کله چې د بودجه ثابت شوی، د ارزانه multimodal پړاو د لوړو محدودیت لري. Runtime د پیسو وړ دی. Scoring ده د لنډي پټ؛ segmentation ده د لنډي پټ؛ تادیه ده د کوچني ثابت عوامل سره لنډي. مهم تر ټولو: د نمونې کارونکی زما سیسټم د هغه څه په څیر چلند کوي چې زه کولی شي کار وکړي. د 12 دقیقې ویډیو په اړه د بحث په ځای کې چې آیا "لره اوږد" دی، زه فیصلہ کولی شئ چې زه غواړم چې کتنه واخلئ. د تحلیلر د ریکارډونو په برخه کې چې په واقعیت کې بدلون کوي، او دا د سټیک UI لپاره پیسې نه ورکوي. دا ټول موضوع دی: د تعقیب توزیع د عادلانه نمونې څخه ګټه ورکوي.