Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Enhancing Applications with Accessibility API
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
Kishikawa Katsumi
March 20, 2024
Programming
5.9k
3
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Enhancing Applications with Accessibility API
Kishikawa Katsumi
March 20, 2024
More Decks by Kishikawa Katsumi
See All by Kishikawa Katsumi
OCRを使ってゲームのアイテムをデータ化する
kishikawakatsumi
0
150
Running Swift without an OS
kishikawakatsumi
0
960
浮動小数の比較について
kishikawakatsumi
0
580
Automatic Grammar Agreementと Markdown Extended Attributes について
kishikawakatsumi
0
260
愛される翻訳の秘訣
kishikawakatsumi
3
460
Private APIの呼び出し方
kishikawakatsumi
3
1k
iOSでSVG画像を扱う
kishikawakatsumi
0
250
Build your own WebP codec in Swift
kishikawakatsumi
2
2.4k
iOSDC 2024 SMBファイル共有をSwiftで実装する
kishikawakatsumi
1
330
Other Decks in Programming
See All in Programming
なぜ型を書くのか? TSKaigi2026で改めて考える #tskaigi_smarthr
kajitack
0
350
コーディングルールの鮮度を保ちたい for SRE NEXT 2026 / keep-fresh-go-internal-conventions-sre-next-2026
handlename
0
140
Embedded SREと共に達成した会員管理システムのAWS移行 - SRE NEXT 2026 ランチスポンサーセッション
niftycorp
PRO
1
2.4k
『コードを書く以外の』エンジニアリング〜課金基盤移行プロジェクト推進のためのTips4選
yuriko1211
0
440
The Past, Present, and Future of Enterprise Java
ivargrimstad
0
170
過去最大のMCPアップデート! 2026-07-28 RC版の謎に迫る
licux
6
460
Generative UI & AI-Assistants for Your Angular Solutions
manfredsteyer
PRO
1
170
えっ!!コードを読まずに開発を!?
hananouchi
0
200
Welcome to the "Parametricity" 🏙️ − Generic だけど Specific な世界 −
guvalif
PRO
1
160
Prismを使った型安全な暗号化_関数型まつり2026
_fhhmm
0
130
【やさしく解説 設計編・中級 #1】一つの車に、運転手は一人 ~ある倉庫システムの事例から~
panda728
PRO
0
170
ルールを書いて終わらせないハーネスエンジニアリング
yug1224
2
990
Featured
See All Featured
Stewardship and Sustainability of Urban and Community Forests
pwiseman
0
320
Ruling the World: When Life Gets Gamed
codingconduct
0
280
Bridging the Design Gap: How Collaborative Modelling removes blockers to flow between stakeholders and teams @FastFlow conf
baasie
0
610
Google's AI Overviews - The New Search
badams
0
1.1k
Sam Torres - BigQuery for SEOs
techseoconnect
PRO
0
300
Designing for humans not robots
tammielis
254
26k
How to Build an AI Search Optimization Roadmap - Criteria and Steps to Take #SEOIRL
aleyda
1
2.1k
Self-Hosted WebAssembly Runtime for Runtime-Neutral Checkpoint/Restore in Edge–Cloud Continuum
chikuwait
0
650
Music & Morning Musume
bryan
47
7.3k
Claude Code どこまでも/ Claude Code Everywhere
nwiizo
65
56k
The Impact of AI in SEO - AI Overviews June 2024 Edition
aleyda
5
1.1k
The Curious Case for Waylosing
cassininazir
1
430
Transcript
&OIBODJOH"QQMJDBUJPOTXJUI "DDFTTJCJMJUZ"1* LJTIJLBXBLBUTVNJ !LJTIJLBXBLBUTVNJ!IBDIZEFSNJP LJTIJLBXBLBUTVNJ
IUUQTDPEFWJTVBMTUVEJPDPNVQEBUFTW@@TUSFBNJOHJOMJOFDIBU
IUUQTDPEFWJTVBMTUVEJPDPNVQEBUFTW@@IPMEUPTQFBLNPEF
*OUFHSBUF"*"TTJTUBOUT*OUP"OZ"QQT r *OZPVSGBWPSJUFUFYUFEJUPS r *OZPVSXFCCSPXTFS r 0SDPNJDSFBEFST
4BNQMF$PEF ➡IUUQTHJUIVCDPNLJTIJLBXBLBUTVNJUSZTXJGU
None
$PSF5FDIOPMPHJFT r 3FUSJFWFTFMFDUFEUFYUGPSBOZBQQMJDBUJPO r .POJUPSHMPCBMLFZNPVTFFWFOUT
"DDFTTJCJMJUZ"1* r "MMPXTBDDFTTUP6*FMFNFOUTPGUIFTZTUFNPSPUIFSBQQMJDBUJPOT r &OBCMFTNBOJQVMBUJPOPG6*FMFNFOUTPGPUIFSBQQMJDBUJPOT
&OBCMJOH"DDFTTJCJMJUZ"1* 4BOECPYBOE1SJWBDZ4FUUJOHT r %JTBCMF4BOECPY r (SBOU"DDFTTJCJMJUZ"1*1FSNJTTJPOT
%JTBCMF4BOECPY
(SBOU"DDFTTJCJMJUZ1FSNJTTJPO 1SJWBDZ4FUUJOHT
(SBOU"DDFTTJCJMJUZ1FSNJTTJPO "9*41SPDFTT5SVTUFE8JUI0QUJPOT @ [kAXTrustedCheckOptionPrompt: true]
(SBOU"DDFTTJCJMJUZ1FSNJTTJPO "9*41SPDFTT5SVTUFE8JUI0QUJPOT @
(SBOU"DDFTTJCJMJUZ1FSNJTTJPO "9*41SPDFTT5SVTUFE8JUI0QUJPOT @
(SBOU"DDFTTJCJMJUZ1FSNJTTJPO "9*41SPDFTT5SVTUFE8JUI0QUJPOT @
#BTJD6TBHF "96*&MFNFOU$PQZ"UUSJCVUF7BMVF @@@
#BTJD6TBHF "96*&MFNFOU$PQZ"UUSJCVUF7BMVF @@@ 5IF6*FMFNFOUDPOUBJOJOHUIFWBMVFZPVXBOUUPSFUSJFWF
#BTJD6TBHF "96*&MFNFOU$PQZ"UUSJCVUF7BMVF @@@ ,FZPGUIFWBMVFZPVXBOUUPSFUSJFWF
#BTJD6TBHF "96*&MFNFOU$PQZ"UUSJCVUF7BMVF @@@ 7BMVFZPVXBOUUPSFUSJFWF
#BTJD6TBHF "96*&MFNFOU$PQZ"UUSJCVUF7BMVF @@@ &SSPSDPEF
3FUSJFWF4FMFDUFE5FYU L"94FMFDUFE5FYU"UUSJCVUF var selectedTextValue: AnyObject? let selectedTextValueError = AXUIElementCopyAttributeValue( focusedElement
as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedTextValue ) guard let selectedTextValue, selectedTextValueError == .success else { return }
3FUSJFWF4FMFDUFE5FYU L"94FMFDUFE5FYU"UUSJCVUF var selectedTextValue: AnyObject? let selectedTextValueError = AXUIElementCopyAttributeValue( focusedElement
as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedTextValue ) guard let selectedTextValue, selectedTextValueError == .success else { return }
3FUSJFWF4FMFDUFE5FYU L"94FMFDUFE5FYU"UUSJCVUF var selectedTextValue: AnyObject? let selectedTextValueError = AXUIElementCopyAttributeValue( focusedElement
as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedTextValue ) guard let selectedTextValue, selectedTextValueError == .success else { return }
4ZTUFN8JEF"96*&MFNFOU "96*&MFNFOU$SFBUF4ZTUFN8JEF let systemWideElement: AXUIElement = AXUIElementCreateSystemWide()
4ZTUFN8JEF"96*&MFNFOU r "DDFTTTZTUFNXJEFLFZCPBSEGPDVT NPVTFDVSTPSQPTJUJPO BOEPUIFS JOGPSNBUJPO r "DDFTTTZTUFNMFWFM6*FMFNFOUTUIBUEPOPUCFMPOHUPBTQFDJ fi D
BQQMJDBUJPO FH NFOVCBS OPUJ fi DBUJPODFOUFS r 3FUSJFWFJOGPSNBUJPOBCPVUUIFDPOUFYUPGUIFDVSSFOUVTFSJOUFSBDUJPO TVDIBTUIFBDUJWFXJOEPXPSUIF6*FMFNFOUUIBUDVSSFOUMZIBTGPDVT
3FUSJFWF'PDVTFE6*&MFNFOU L"9'PDVTFE6*&MFNFOU"UUSJCVUF let systemWideElement = AXUIElementCreateSystemWide() var focusedElement: AnyObject? let
focusedElementError = AXUIElementCopyAttributeValue( systemWideElement, kAXFocusedUIElementAttribute as CFString, &focusedElement )
3FUSJFWF4FMFDUFE5FYU L"94FMFDUFE5FYU"UUSJCVUF var selectedTextValue: AnyObject? let selectedTextValueError = AXUIElementCopyAttributeValue( focusedElement
as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedTextValue ) guard let selectedTextValue, selectedTextValueError == .success else { return }
None
.POJUPS(MPCBM,FZ&WFOU /4&WFOUBEE(MPCBM.POJUPS'PS&WFOUT NBUDIJOHIBOEMFS open class NSEvent : NSObject, NSCopying, NSCoding
{ open class func addGlobalMonitorForEvents( matching mask: NSEvent.EventTypeMask, handler block: @escaping (NSEvent) -> Void ) -> Any? }
.POJUPS(MPCBM,FZ&WFOU NSEvent.addGlobalMonitorForEvents( matching: [.keyDown] ) { (event) in switch event.type
{ case .keyDown: guard event.modifierFlags.contains([.command, .control]) && event.keyCode == kVK_ANSI_A else { return } ... default: break } }
.POJUPS(MPCBM,FZ&WFOU NSEvent.addGlobalMonitorForEvents( matching: [.keyDown] ) { (event) in switch event.type
{ case .keyDown: guard event.modifierFlags.contains([.command, .control]) && event.keyCode == kVK_ANSI_A else { return } ... default: break } }
.POJUPS(MPCBM,FZ&WFOU $BSCPO"1* *OTUBMM&WFOU)BOEMFS 3FHJTUFS&WFOU)PU,FZ ➡IUUQTHJUIVCDPNLJTIJLBXBLBUTVNJUSZTXJGU r *OTUBMM&WFOU)BOEMFS @@@@@@
r 3FHJTUFS&WFOU)PU,FZ @@@@@@ r $BSCPO"1* r /PBDDFTTJCJMJUZQFSNJTTJPOSFRVJSFE
.POJUPS(MPCBM,FZ&WFOU &WFOU5BQ ➡IUUQTHJUIVCDPNLJTIJLBXBLBUTVNJUSZTXJGU let mask: CGEventMask = (1 << CGEventType.keyDown.rawValue)
let tap = CGEvent.tapCreate( tap: .cgSessionEventTap, place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: mask, callback: { (proxy, type, event, refcon) in ... }, userInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) ) let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) CGEvent.tapEnable(tap: tap!, enable: true)
.POJUPS(MPCBM,FZ&WFOU ➡IUUQTHJUIVCDPNLJTIJLBXBLBUTVNJUSZTXJGU r $BOTUPQFWFOUQSPQBHBUJPO r $BONPEJGZFWFOU &WFOU5BQ
None
3FUSJFWF4FMFDUFE5FYU 'SPN8FC7JFX var selectedTextMarkerRangeValue: AnyObject? let selectedTextMarkerRangeValueError = AXUIElementCopyAttributeValue( focusedElement
as! AXUIElement, "AXSelectedTextMarkerRange" as CFString, &selectedTextMarkerRangeValue ) ... var stringForTextMarkerRangeValue: AnyObject? let stringForTextMarkerRangeValueError = AXUIElementCopyParameterizedAttributeValue( focusedElement as! AXUIElement, "AXStringForTextMarkerRange" as CFString, selectedTextMarkerRangeValue, &stringForTextMarkerRangeValue ) ...
3FUSJFWF4FMFDUFE5FYU 'SPN8FC7JFX var selectedTextMarkerRangeValue: AnyObject? let selectedTextMarkerRangeValueError = AXUIElementCopyAttributeValue( focusedElement
as! AXUIElement, "AXSelectedTextMarkerRange" as CFString, &selectedTextMarkerRangeValue ) ... var stringForTextMarkerRangeValue: AnyObject? let stringForTextMarkerRangeValueError = AXUIElementCopyParameterizedAttributeValue( focusedElement as! AXUIElement, "AXStringForTextMarkerRange" as CFString, selectedTextMarkerRangeValue, &stringForTextMarkerRangeValue ) ...
None
"96*&MFNFOU$PQZ1BSBNFUFSJ[FE"UUSJCVUF7BMVF var selectedTextMarkerRangeValue: AnyObject? let selectedTextMarkerRangeValueError = AXUIElementCopyAttributeValue( focusedElement as!
AXUIElement, "AXSelectedTextMarkerRange" as CFString, &selectedTextMarkerRangeValue ) ... var stringForTextMarkerRangeValue: AnyObject? let stringForTextMarkerRangeValueError = AXUIElementCopyParameterizedAttributeValue( focusedElement as! AXUIElement, "AXStringForTextMarkerRange" as CFString, selectedTextMarkerRangeValue, &stringForTextMarkerRangeValue ) ...
var selectedTextMarkerRangeValue: AnyObject? let selectedTextMarkerRangeValueError = AXUIElementCopyAttributeValue( focusedElement as! AXUIElement,
"AXSelectedTextMarkerRange" as CFString, &selectedTextMarkerRangeValue ) ... var stringForTextMarkerRangeValue: AnyObject? let stringForTextMarkerRangeValueError = AXUIElementCopyParameterizedAttributeValue( focusedElement as! AXUIElement, "AXStringForTextMarkerRange" as CFString, selectedTextMarkerRangeValue, &stringForTextMarkerRangeValue ) ... "96*&MFNFOU$PQZ1BSBNFUFSJ[FE"UUSJCVUF7BMVF
var selectedTextMarkerRangeValue: AnyObject? let selectedTextMarkerRangeValueError = AXUIElementCopyAttributeValue( focusedElement as! AXUIElement,
"AXSelectedTextMarkerRange" as CFString, &selectedTextMarkerRangeValue ) ... var stringForTextMarkerRangeValue: AnyObject? let stringForTextMarkerRangeValueError = AXUIElementCopyParameterizedAttributeValue( focusedElement as! AXUIElement, "AXStringForTextMarkerRange" as CFString, selectedTextMarkerRangeValue, &stringForTextMarkerRangeValue ) ... "96*&MFNFOU$PQZ1BSBNFUFSJ[FE"UUSJCVUF7BMVF
)PXUP,OPX6OEPDVNFOUFE,FZT var selectedTextMarkerRangeValue: AnyObject? let selectedTextMarkerRangeValueError = AXUIElementCopyAttributeValue( focusedElement as!
AXUIElement, "AXSelectedTextMarkerRange" as CFString, &selectedTextMarkerRangeValue ) ... var stringForTextMarkerRangeValue: AnyObject? let stringForTextMarkerRangeValueError = AXUIElementCopyParameterizedAttributeValue( focusedElement as! AXUIElement, "AXStringForTextMarkerRange" as CFString, selectedTextMarkerRangeValue, &stringForTextMarkerRangeValue ) ...
)PXUP,OPX6OEPDVNFOUFE,FZT "96*&MFNFOU$PQZ"UUSJCVUF/BNFT @@
None
None
/PUJGJDBUJPOT var observer: AXObserver? let observerCreateError = AXObserverCreate( app.processIdentifier, {
(observer, element, notification, userData) in ... }, &observer ) AXObserverAddNotification( observer, appElement, kAXSelectedTextChangedNotification as CFString, nil ) CFRunLoopAddSource( CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(observer), .commonModes )
/PUJGJDBUJPOT var observer: AXObserver? let observerCreateError = AXObserverCreate( app.processIdentifier, {
(observer, element, notification, userData) in ... }, &observer ) AXObserverAddNotification( observer, appElement, kAXSelectedTextChangedNotification as CFString, nil ) CFRunLoopAddSource( CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(observer), .commonModes )
/PUJGJDBUJPOT var observer: AXObserver? let observerCreateError = AXObserverCreate( app.processIdentifier, {
(observer, element, notification, userData) in ... }, &observer ) AXObserverAddNotification( observer, appElement, kAXSelectedTextChangedNotification as CFString, nil ) CFRunLoopAddSource( CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(observer), .commonModes ) let app = NSRunningApplication .runningApplications(withBundleIdentifier: "com.apple.TextEdit") .first
/PUJGJDBUJPOT var observer: AXObserver? let observerCreateError = AXObserverCreate( app.processIdentifier, {
(observer, element, notification, userData) in ... }, &observer ) AXObserverAddNotification( observer, appElement, kAXSelectedTextChangedNotification as CFString, nil ) CFRunLoopAddSource( CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(observer), .commonModes )
/PUJGJDBUJPOT var observer: AXObserver? let observerCreateError = AXObserverCreate( app.processIdentifier, {
(observer, element, notification, userData) in ... }, &observer ) AXObserverAddNotification( observer, appElement, kAXSelectedTextChangedNotification as CFString, nil ) CFRunLoopAddSource( CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(observer), .commonModes ) kAXSelectedTextChangedNotification kAXValueChangedNotification ...
.PEJGZ5FYU var isAttributeSettableValue = DarwinBoolean(false) let isAttributeSettableValueError = AXUIElementIsAttributeSettable( element,
kAXValueAttribute as CFString, &isAttributeSettableValue ) guard isAttributeSettableValueError == .success else { return } if isAttributeSettableValue.boolValue { AXUIElementSetAttributeValue( element, kAXValueAttribute as CFString, "\(precedingTextValue)\n\(codeBlock)" as CFString ) return }
.PEJGZ5FYU var isAttributeSettableValue = DarwinBoolean(false) let isAttributeSettableValueError = AXUIElementIsAttributeSettable( element,
kAXValueAttribute as CFString, &isAttributeSettableValue ) guard isAttributeSettableValueError == .success else { return } if isAttributeSettableValue.boolValue { AXUIElementSetAttributeValue( element, kAXValueAttribute as CFString, "\(precedingTextValue)\n\(codeBlock)" as CFString ) return }
.PEJGZ5FYU var isAttributeSettableValue = DarwinBoolean(false) let isAttributeSettableValueError = AXUIElementIsAttributeSettable( element,
kAXValueAttribute as CFString, &isAttributeSettableValue ) guard isAttributeSettableValueError == .success else { return } if isAttributeSettableValue.boolValue { AXUIElementSetAttributeValue( element, kAXValueAttribute as CFString, "\(precedingTextValue)\n\(codeBlock)" as CFString ) return }
None
None
4$4DSFFOTIPU.BOBHFSDBQUVSF*NBHF func captureScreen(rect: CGRect) async throws -> CGImage? {
let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: false) guard let mainDisplayID = NSScreen.main?.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID, let display = content.displays.first(where: { $0.displayID == mainDisplayID }) else { return nil } let streamConfig = SCStreamConfiguration() streamConfig.captureResolution = .automatic streamConfig.sourceRect = rect streamConfig.preservesAspectRatio = true streamConfig.scalesToFit = true streamConfig.capturesAudio = false streamConfig.excludesCurrentProcessAudio = true let filter = SCContentFilter(display: display, excludingWindows: []) let capturedImage = try await SCScreenshotManager.captureImage( contentFilter: filter, configuration: streamConfig ) return capturedImage }
3FUSJFWFBO*NBHF $(8JOEPX-JTU$SFBUF*NBHF @@@@ func captureScreen(rect: CGRect) -> CGImage? { let
screenShot = CGWindowListCreateImage( rect, [.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID, [.boundsIgnoreFraming, .bestResolution] ) return screenShot }
(SBOU4DSFFO3FDPSEJOH1FSNJTTJPO 1SJWBDZ4FUUJOHT
(SBOU4DSFFO3FDPSEJOH1FSNJTTJPO $(1SF fl JHIU4DSFFO$BQUVSF"DDFTT r $(1SF fl JHIU4DSFFO$BQUVSF"DDFTT r
*OEJDBUFTXIFUIFSTDSFFOSFDPSEJOHJTBMMPXFE r $(3FRVFTU4DSFFO$BQUVSF"DDFTT r 1SFTFOUUIFTZTUFNQSPNQU
&YUSBDUUIF5FYUGSPNBO*NBHF ϒϥοΫδϟοΫʹΑΖ͘͠ ࠤ౻लๆ
7JTJPO,JU ϒϥοΫδϟοΫʹΑΖ͘͠ ࠤ౻लๆ
&YUSBDUUIF5FYUGSPNBO*NBHF *NBHF"OBMZ[FS let analyzer = ImageAnalyzer() let analysis = try
await analyzer.analyze( image, orientation: .up, configuration: configuration ) let transcript = analysis.transcript
&YUSBDUUIF5FYUGSPNBO*NBHF *NBHF"OBMZ[FS let analyzer = ImageAnalyzer() let analysis = try
await analyzer.analyze( image, orientation: .up, configuration: configuration ) let transcript = analysis.transcript
None
8SBQ6Q r 8JOEPX$IBU"TTJTUBOU r #BTJD6TBHFPG"DDFTTJCJMJUZ"1* r *OMJOF$IBU"TTJTUBOU r /PUJ fi
DBUJPOT r .PEJGZ$POUFOU r $PNJD5SBOTMBUPS r 4DSFFO$BQUVSF"1*BOE0$3 r "96*&MFNFOU*OTQFDUPS r *OTQFDU"UUSJCVUF/BNFPG"96*&MFNFOU
4BNQMF$PEF ➡IUUQTHJUIVCDPNLJTIJLBXBLBUTVNJUSZTXJGU
3FGFSFODFT r 4BNQMF$PEF IUUQTHJUIVCDPNLJTIJLBXBLBUTVNJUSZTXJGU r "DDFTTJOHUFYUWBMVFGSPNBOZ4ZTUFNXJEF"QQMJDBUJPOWJB"DDFTTJCJMJUZ "1* IUUQTNBDEFWFMPQFSTXPSEQSFTTDPNBDDFTTJOHUFYUWBMVFGSPNBOZTZTUFNXJEFBQQMJDBUJPOWJB BDDFTTJCJMJUZBQJ r
)PXUPHFUTFMFDUFEUFYUBOEJUTDPPSEJOBUFTGSPNBOZTZTUFNXJEFBQQMJDBUJPOVTJOH "DDFTTJCJMJUZ "1* IUUQTNBDEFWFMPQFSTXPSEQSFTTDPNIPXUPHFUTFMFDUFEUFYUBOEJUTDPPSEJOBUFTGSPNBOZTZTUFNXJEF BQQMJDBUJPOVTJOHBDDFTTJCJMJUZBQJ r 8IZBSFOUUIFNPTUVTFGVM.BDBQQTPOUIF"QQ4UPSF IUUQTBMJOQBOBJUJVDPNCMPHBQQTPVUTJEFBQQTUPSF r "MMBCPVUNBD04FWFOUPCTFSWBUJPO IUUQTEPDTHPPHMFDPNQSFTFOUBUJPOEO&BJ16EVIWKLTS%735D+B&6-C488IU7E()'@946IUNMQSFTFOU