<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Playwright on 42Class</title><link>https://42class.com/tags/playwright/</link><description>Recent content in Playwright on 42Class</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Sun, 12 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://42class.com/tags/playwright/index.xml" rel="self" type="application/rss+xml"/><item><title>Meetnote v0.3.0 — 직접 쓰면서 고친 것들</title><link>https://42class.com/posts/meetnote-v030-improvements/</link><pubDate>Sun, 12 Apr 2026 00:00:00 +0000</pubDate><guid>https://42class.com/posts/meetnote-v030-improvements/</guid><description>&lt;p&gt;&lt;a class="link" href="https://42class.com/posts/meetnote-retrospective/" &gt;지난 글&lt;/a&gt;에서 Meetnote를 왜, 어떻게 만들었는지를 썼다. 일주일 만에 MVP를 완성했고, 꽤 만족스러웠다. 그런데 직접 쓰기 시작하니까 바로 문제가 보였다.&lt;/p&gt;
&lt;p&gt;회의가 길어지면 중간에 쉬는 시간이 생기는데, 녹음을 끊고 새로 시작하면 회의록이 두 개로 쪼개진다. 반대로 쉬는 시간까지 녹음하면 Whisper가 무음 구간에서 환각을 일으킨다. 사이드 패널에서 회의가 10개를 넘으면 잘렸고, 요약 파서는 마크다운 헤딩 하나만 바뀌어도 깨졌다. &amp;ldquo;잘 되는데?&amp;rdquo; 싶었던 것들이 실전에서 하나둘 무너졌다.&lt;/p&gt;
&lt;p&gt;v0.3.0은 그렇게 직접 쓰면서 발견한 문제들을 하나씩 고친 버전이다.&lt;/p&gt;
&lt;h2 id="일시정지와-재개--회의는-끊기지-않는다"&gt;일시정지와 재개 — 회의는 끊기지 않는다
&lt;/h2&gt;&lt;p&gt;가장 먼저 필요했던 건 녹음 일시정지였다. 회의 중간에 5분 휴식, 잠깐 자리 비움 — 이런 상황이 매일 있다. 그런데 기존에는 선택지가 두 개뿐이었다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;녹음을 끊고 새로 시작한다 → 회의록이 분리됨&lt;/li&gt;
&lt;li&gt;그냥 계속 녹음한다 → 무음 구간에서 환각 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;둘 다 별로다. 일시정지가 있으면 깔끔하게 해결된다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://42class.com/img/meetnote-recording.png" alt="녹음 중 상태 — 일시정지 버튼과 경과 시간이 표시된다" loading="lazy" class="gallery-image"&gt;
&lt;/p&gt;
&lt;p&gt;구현 자체는 단순했다. &lt;code&gt;RecordingSession&lt;/code&gt;에 &lt;code&gt;paused&lt;/code&gt; 플래그를 추가하고, 일시정지 상태에서는 오디오 버퍼에 데이터를 쌓지 않으면 된다. 그런데 타이머에서 문제가 터졌다.&lt;/p&gt;
&lt;p&gt;사이드 패널 헤더의 타이머는 벽시계(wall-clock) 기준이었고, 상태 바의 타이머는 경과 시간(elapsed) 기준이었다. 일시정지 후 재개하면 헤더는 점프하고 상태 바는 0으로 리셋됐다. 같은 녹음인데 두 곳에서 다른 시간을 보여주는 건 사용자를 혼란스럽게 만든다.&lt;/p&gt;
&lt;p&gt;해결은 단일 시간 소스(single source of truth)로 통일하는 것이었다. &lt;code&gt;plugin.getRecordedElapsedMs()&lt;/code&gt; 하나만 바라보게 만들어서, 어디서든 같은 경과 시간을 표시하게 했다.&lt;/p&gt;
&lt;h2 id="이어-녹음--1-md--1-회의-원칙"&gt;이어 녹음 — 1 MD = 1 회의 원칙
&lt;/h2&gt;&lt;p&gt;일시정지만으로는 부족한 경우가 있었다. 오전 회의가 끝나고, 오후에 같은 주제로 이어서 회의할 때. 또는 녹음을 실수로 중지했을 때. 이런 상황에서 회의록이 쪼개지면 맥락이 끊긴다.&lt;/p&gt;
&lt;p&gt;그래서 &amp;ldquo;이어 녹음&amp;rdquo; 기능을 만들었다. 핵심 원칙은 &lt;strong&gt;1 MD = 1 회의&lt;/strong&gt;. 하나의 마크다운 문서가 하나의 논리적 회의를 대표한다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://42class.com/img/meetnote-sidepanel.png" alt="사이드 패널 — 이어 녹음 버튼과 참석자 관리 화면" loading="lazy" class="gallery-image"&gt;
&lt;/p&gt;
&lt;p&gt;기술적으로는 이렇게 동작한다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&amp;ldquo;이어 녹음&amp;rdquo; 버튼을 누르면 새 WAV 파일이 생성되지만, &lt;code&gt;meta.json&lt;/code&gt;의 &lt;code&gt;document_path&lt;/code&gt;는 원본 회의록을 가리킨다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;continued_from&lt;/code&gt; 필드로 원본 녹음과 연결된다&lt;/li&gt;
&lt;li&gt;처리 시 &lt;code&gt;_concatenate_wavs()&lt;/code&gt;로 WAV를 병합한 뒤, 하나의 전사 결과를 생성한다&lt;/li&gt;
&lt;li&gt;사이드 패널에서는 &lt;code&gt;_aggregate_recordings_by_document()&lt;/code&gt;로 같은 &lt;code&gt;document_path&lt;/code&gt;를 가진 WAV들을 묶어서 하나의 항목으로 표시한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;여기서 가장 까다로웠던 건 삭제와 재처리의 연쇄(cascade) 처리였다. 원본 녹음을 삭제하면 이어 녹음한 WAV도 같이 삭제되어야 한다. 재처리를 요청하면 관련된 모든 WAV의 &lt;code&gt;.done&lt;/code&gt; 마커와 처리 결과를 초기화해야 한다. &lt;code&gt;_find_related_recordings()&lt;/code&gt; 함수가 &lt;code&gt;document_path&lt;/code&gt;를 기준으로 관련 파일을 찾아서 일괄 처리한다.&lt;/p&gt;
&lt;p&gt;처음에는 WAV 파일 단위로 사이드 패널에 표시했는데, 같은 회의가 두 번 나타나는 버그가 있었다. 문서 단위 집계로 바꾸면서 해결했다.&lt;/p&gt;
&lt;h2 id="운영-버그--실전에서만-보이는-것들"&gt;운영 버그 — 실전에서만 보이는 것들
&lt;/h2&gt;&lt;p&gt;직접 쓰면서 발견한 버그들은 테스트 환경에서는 재현하기 어려운 것들이었다.&lt;/p&gt;
&lt;h3 id="요약-파서의-취약성"&gt;요약 파서의 취약성
&lt;/h3&gt;&lt;p&gt;Claude가 생성하는 요약의 형식이 미묘하게 달라지는 경우가 있었다. &lt;code&gt;##&lt;/code&gt; 대신 &lt;code&gt;###&lt;/code&gt;을 쓰거나, 코드 펜스 안에 요약 키워드가 들어가거나, 요약 전에 서문을 붙이거나. 파서는 정확한 형식만 기대하고 있었기 때문에 조금만 달라져도 실패했다.&lt;/p&gt;
&lt;p&gt;수정 후 파서는 &lt;code&gt;##&lt;/code&gt;/&lt;code&gt;###&lt;/code&gt; 모두 처리하고, 코드 펜스를 건너뛰고, 서문을 무시하고, 레이블의 변형(&lt;code&gt;|&lt;/code&gt; 구분 대체 표현)까지 수용한다. 그리고 실패 원인을 4가지로 분류해서(&lt;code&gt;no-transcript&lt;/code&gt;, &lt;code&gt;engine-missing&lt;/code&gt;, &lt;code&gt;generation-failed&lt;/code&gt;, &lt;code&gt;parse-failed&lt;/code&gt;) 각각에 맞는 안내 메시지를 마크다운에 남긴다. &amp;ldquo;요약 실패&amp;quot;라는 뭉뚱그린 메시지 대신 뭐가 왜 안 됐는지 알 수 있게 했다.&lt;/p&gt;
&lt;h3 id="화자-라벨의-한국어-조사-문제"&gt;화자 라벨의 한국어 조사 문제
&lt;/h3&gt;&lt;p&gt;화자 등록 후 &amp;ldquo;화자1&amp;quot;을 실제 이름으로 치환할 때, 한국어 조사가 붙은 경우를 놓치고 있었다. &amp;ldquo;화자1이 말했다&amp;rdquo;, &amp;ldquo;화자1의 의견&amp;rdquo; 같은 패턴에서 &amp;ldquo;화자1&amp;rdquo; 부분만 바뀌고 조사는 그대로 남았다.&lt;/p&gt;
&lt;p&gt;더 까다로운 건 &amp;ldquo;화자1&amp;quot;과 &amp;ldquo;화자10&amp;quot;의 구분이다. 단순 문자열 치환으로는 &amp;ldquo;화자10&amp;quot;을 처리할 때 &amp;ldquo;화자1&amp;quot;까지 잘못 치환된다. negative-lookahead 정규식으로 뒤에 숫자가 이어지지 않는 경우만 매칭하게 해서 해결했다.&lt;/p&gt;
&lt;h3 id="사이드-패널의-소소한-불편들"&gt;사이드 패널의 소소한 불편들
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;10개 제한&lt;/strong&gt;: 완료된 회의가 10개를 넘으면 나머지가 안 보였다. 하드코딩된 제한을 제거&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;이메일 설정 누락 알림&lt;/strong&gt;: 발신자 이메일이 설정 안 되어 있으면 버튼만 비활성화되고 이유를 몰랐다. 배너로 안내 추가&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;대기 중 결과 자동 갱신&lt;/strong&gt;: &lt;code&gt;pickupPendingResults()&lt;/code&gt; 후 패널이 갱신되지 않아서 수동 새로고침이 필요했다. 자동 갱신 처리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;마크다운 파일 누락 처리&lt;/strong&gt;: 파일이 삭제/이동되었을 때 조용히 실패하던 것을 Notice로 사용자에게 알림&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;.env&lt;/code&gt; 인용 부호&lt;/strong&gt;: &lt;code&gt;.env&lt;/code&gt; 파일에서 값을 따옴표로 감싸면 따옴표까지 값으로 인식해서 SMTP 인증이 실패했다. 자동 제거 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하나하나는 작은 문제지만, 이런 것들이 쌓이면 &amp;ldquo;불안정한 도구&amp;quot;라는 인상을 준다.&lt;/p&gt;
&lt;h2 id="playwright-e2e-테스트--0-수동-테스트"&gt;Playwright E2E 테스트 — 0 수동 테스트
&lt;/h2&gt;&lt;p&gt;버그를 고치면서 점점 불안해졌다. 하나를 고치면 다른 데서 터질 것 같았다. 수동으로 매번 전체 시나리오를 확인하는 건 불가능했다. 테스트 자동화가 필요했다.&lt;/p&gt;
&lt;p&gt;백엔드는 pytest로 비교적 쉬웠다. 74개 테스트가 API 엔드포인트, 보안, 엣지 케이스를 커버한다. 문제는 프론트엔드였다.&lt;/p&gt;
&lt;p&gt;Meetnote의 프론트엔드는 Obsidian 플러그인이다. 브라우저 앱이 아니라 Electron 기반 데스크톱 앱 위에서 돌아간다. 일반적인 웹 E2E 테스트 프레임워크를 그대로 쓸 수 없다. 그래서 &lt;strong&gt;Playwright + Obsidian CDP(Chrome DevTools Protocol)&lt;/strong&gt; 조합을 택했다.&lt;/p&gt;
&lt;p&gt;Obsidian은 Electron 앱이므로 &lt;code&gt;--remote-debugging-port&lt;/code&gt; 옵션으로 CDP를 열 수 있다. Playwright가 CDP를 통해 Obsidian에 연결하면, 실제 Obsidian 환경에서 플러그인을 조작하고 결과를 검증할 수 있다.&lt;/p&gt;
&lt;h3 id="테스트-구조"&gt;테스트 구조
&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;plugin/tests/
├── SCENARIOS.md # 시나리오 레지스트리 (50개, 단일 진실 공급원)
├── e2e/
│ ├── 01-side-panel.spec.ts # S1-S2: 패널 상태
│ ├── 02-recording-flow.spec.ts # S4: 녹음→일시정지→재개→중지
│ ├── 03-speakers.spec.ts # S8-S9: 화자 DB
│ ├── 04-rename-processing.spec.ts # S5-S6: 이름 변경, 진행 바
│ ├── 05-edge-cases.spec.ts # S3, S13-S20: 오프라인, 중복 요청
│ ├── 06-participants.spec.ts # S7, S24-S27: 참석자 관리
│ ├── 07-recording-list.spec.ts # S28-S42: 목록, 집계, 이어 녹음
│ ├── 08-summary-rendering.spec.ts # S32-S35: 요약 렌더링
│ └── 99-happy-path.spec.ts # H1-H8: 실제 오디오 전체 파이프라인
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;SCENARIOS.md&lt;/code&gt;가 단일 진실 공급원(single source of truth) 역할을 한다. 50개 시나리오 각각에 ID, 설명, 매핑된 테스트 파일이 명시되어 있다. 새 기능을 추가하면 시나리오를 먼저 등록하고, 테스트를 작성하고, 구현한다.&lt;/p&gt;
&lt;h3 id="happy-path--실제-오디오로-전체-검증"&gt;Happy Path — 실제 오디오로 전체 검증
&lt;/h3&gt;&lt;p&gt;가장 공들인 건 &lt;code&gt;99-happy-path.spec.ts&lt;/code&gt;다. 실제 녹음된 WAV 파일을 테스트 픽스처로 사용해서, 운영 환경과 동일한 흐름을 자동으로 검증한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;H1. WAV + meta.json 셋업 → 처리 버튼 클릭
H2. STT + 화자 분리 완료 대기
H3. Claude CLI 요약 생성
H4. 화자 등록 → &amp;#34;화자N&amp;#34; 라벨 자동 치환 확인
H5. 수동 참석자 추가
H6. 최종 마크다운 검증 — 4개 요약 섹션, 전체 전사, 발화 통계, 화자N 라벨 0개
H7. 이메일 발송
H8. 완료된 회의 자동 열기
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 테스트 하나가 돌아가면 &amp;ldquo;오늘 배포해도 된다&amp;quot;는 확신이 생긴다. Whisper 모델 전사, pyannote 화자 분리, 화자 임베딩 매칭, LLM 요약, 라벨 치환, 이메일 발송까지 — 전체 파이프라인이 실제 오디오로 검증된다.&lt;/p&gt;
&lt;p&gt;실제 E2E 테스트가 돌아가는 모습이다:&lt;/p&gt;
&lt;div class="video-wrapper"&gt;
 &lt;iframe loading="lazy" 
 src="https://www.youtube.com/embed/PiEZt408t4Q" 
 allowfullscreen 
 title="YouTube Video"
 &gt;
 &lt;/iframe&gt;
&lt;/div&gt;

&lt;h3 id="테스트-러너"&gt;테스트 러너
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;run-tests.sh&lt;/code&gt;가 4단계 파이프라인을 순서대로 실행한다:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[1/4] Backend pytest — 74개 테스트
[2/4] TypeScript 타입 체크 + 빌드
[3/4] Playwright E2E — 42개 테스트
[4/4] Happy path — 8개 시나리오 (실제 오디오)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;ML 모델 워밍업, 프로세스 격리(좀비 프로세스 방지), Obsidian CDP 자동 재시작까지 포함된 650줄짜리 스크립트다. pre-commit 훅으로 연결해서 커밋 전에 자동 실행된다.&lt;/p&gt;
&lt;p&gt;처음에는 테스트 후 Obsidian과 백엔드 서버 프로세스가 좀비로 남는 문제가 있었다. &lt;code&gt;nohup ... &amp;lt; /dev/null&lt;/code&gt;과 &lt;code&gt;$!&lt;/code&gt; + &lt;code&gt;disown&lt;/code&gt; 패턴으로 프로세스를 격리해서 해결했다. &amp;ldquo;Killed: 9&amp;rdquo; 에러 메시지도 사라졌다.&lt;/p&gt;
&lt;h3 id="수동-테스트-0개"&gt;수동 테스트 0개
&lt;/h3&gt;&lt;p&gt;50개 시나리오 전부 자동화했다. 수동 테스트가 0개다. 이건 단순히 편의의 문제가 아니다. Obsidian 플러그인은 릴리스하면 되돌리기 어렵다. 사용자 환경에서 문제가 발생하면 디버깅도 힘들다. 자동화된 테스트가 배포 전 안전망이 된다.&lt;/p&gt;
&lt;h2 id="사이드-패널-ui-개선"&gt;사이드 패널 UI 개선
&lt;/h2&gt;&lt;p&gt;기능 수정과 함께 UI도 다듬었다. Lucide 아이콘 적용, 오프라인 상태 배너, 경과 시간 표시, Modal 다이얼로그(브라우저 기본 &lt;code&gt;confirm()&lt;/code&gt; 대체) 등 10가지 이상의 소소한 개선을 넣었다.&lt;/p&gt;
&lt;p&gt;개별적으로는 작은 변화지만, 전체적으로 &amp;ldquo;도구답다&amp;quot;는 느낌이 확실히 달라졌다. 특히 &lt;code&gt;confirm()&lt;/code&gt; 대신 Obsidian Modal을 쓰니까 앱의 일부처럼 자연스러워졌다.&lt;/p&gt;
&lt;h2 id="adr-도입--결정의-기록"&gt;ADR 도입 — 결정의 기록
&lt;/h2&gt;&lt;p&gt;v0.3.0부터 Architecture Decision Record를 도입했다. 이어 녹음(ADR-003), 일시정지/재개(ADR-004) 같은 설계 결정을 문서로 남긴다.&lt;/p&gt;
&lt;p&gt;혼자 만드는 프로젝트인데 ADR이 필요한가? 필요했다. 2주 전에 왜 이렇게 만들었는지 기억이 안 나는 순간이 실제로 왔다. &amp;ldquo;document_path 기준으로 집계하는 게 맞았나, WAV 파일 기준이었나?&amp;rdquo; 같은 질문에 ADR이 바로 답해준다.&lt;/p&gt;
&lt;h2 id="숫자로-보는-v030"&gt;숫자로 보는 v0.3.0
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;항목&lt;/th&gt;
 &lt;th&gt;수치&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;변경 파일&lt;/td&gt;
 &lt;td&gt;45개&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;추가&lt;/td&gt;
 &lt;td&gt;+6,601줄&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;삭제&lt;/td&gt;
 &lt;td&gt;-1,010줄&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;백엔드 테스트&lt;/td&gt;
 &lt;td&gt;74개&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;E2E 테스트&lt;/td&gt;
 &lt;td&gt;42개&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Happy path&lt;/td&gt;
 &lt;td&gt;8개&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;총 자동화 테스트&lt;/td&gt;
 &lt;td&gt;124개&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;수동 테스트&lt;/td&gt;
 &lt;td&gt;0개&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;시나리오 레지스트리&lt;/td&gt;
 &lt;td&gt;50개&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="돌아보며"&gt;돌아보며
&lt;/h2&gt;&lt;p&gt;v0.1.0은 &amp;ldquo;돌아가는 것&amp;quot;을 만드는 단계였다. v0.3.0은 &amp;ldquo;쓸 수 있는 것&amp;quot;을 만드는 단계였다. 차이는 크다.&lt;/p&gt;
&lt;p&gt;직접 쓰지 않았으면 발견하지 못했을 문제들이 대부분이었다. 타이머가 점프하는 것, 화자 라벨에 조사가 안 바뀌는 것, 회의 목록이 10개에서 잘리는 것 — 코드 리뷰만으로는 절대 잡을 수 없다. &amp;ldquo;자기가 만든 도구를 자기가 쓴다&amp;quot;는 원칙이 왜 중요한지 체감한 시간이었다.&lt;/p&gt;
&lt;p&gt;Playwright로 Obsidian 플러그인 E2E 테스트를 구축한 건 이번 릴리스에서 가장 뿌듯한 부분이다. Electron 앱 위에서 돌아가는 플러그인이라 테스트 자동화가 까다로울 거라 생각했는데, CDP를 통한 연결이 예상보다 안정적이었다. 실제 오디오 파일로 전체 파이프라인을 검증하는 happy path 테스트가 돌아갈 때, &amp;ldquo;이제 자신 있게 배포할 수 있다&amp;quot;는 느낌이 든다.&lt;/p&gt;
&lt;p&gt;다음은 웹 기반 대시보드와 팀 단위 화자 DB 공유 — 가 될 수도 있고, 또 직접 쓰면서 다른 게 급해질 수도 있다. 그게 이 프로젝트의 방식이다.&lt;/p&gt;
&lt;hr&gt;
&lt;div style="background: linear-gradient(135deg, #24292e 0%, #40464d 100%); border-radius: 12px; padding: 24px 32px; text-align: center; margin-top: 32px;"&gt;
 &lt;p style="color: #f0f0f0; font-size: 1.2em; margin-bottom: 12px; font-weight: bold;"&gt;Meetnote — 로컬 회의록 자동화&lt;/p&gt;
 &lt;p style="color: #8b949e; font-size: 0.9em; margin-bottom: 16px;"&gt;Obsidian plugin for local meeting transcription, speaker diarization, and AI summarization&lt;/p&gt;
 &lt;a href="https://github.com/changsu-jin/meetnote" style="display: inline-block; background: #238636; color: white; padding: 10px 24px; border-radius: 8px; text-decoration: none; font-weight: bold; font-size: 1em;"&gt;GitHub에서 보기 →&lt;/a&gt;
&lt;/div&gt;</description></item></channel></rss>