57 min readAmbient capture

Implementation Guide: Transcribe on-site walkthrough notes and generate punch list items for review

Step-by-step implementation guide for deploying AI to transcribe on-site walkthrough notes and generate punch list items for review for Construction & Contractors clients.

Hardware Procurement

PLAUD Note Pro AI Voice Recorder

PLAUDPLAUD-NOTE-PROQty: 3

$150/unit MSP cost (bulk) / $229-$249 suggested resale with setup

Primary ambient capture device worn clipped to hard hat strap or vest pocket during walkthroughs. Features 4 MEMS microphones with 16.4 ft capture range, 50-hour battery life, and 64GB local storage. Records audio offline and syncs when connectivity is available. One per field superintendent.

Zoom H1essential Portable Recorder

Zoom CorporationH1essentialQty: 1

$80/unit MSP cost (dealer) / $129-$149 suggested resale with setup

Backup/alternative capture device for extremely noisy environments. X/Y stereo microphones with 32-bit float recording handles up to 120 dB SPL without clipping—critical for sites with active jackhammering, saw cutting, or heavy equipment. No gain staging required. Keep one in the site trailer as a shared spare.

Samsung Galaxy XCover7 Pro Rugged Smartphone

SamsungSM-G556BQty: 2

$350/unit MSP cost (business channel) / $499-$549 suggested resale configured

Dedicated fleet device for field superintendents who should not use personal phones for business recordings. IP68/MIL-STD-810H rated, glove-touch support, removable battery. Pre-configured with recording apps, MDM profile, VPN, and review dashboard. Serves as both backup recorder (built-in mic) and the review/approval interface in the field.

ISOtunes PRO 2.5 Bluetooth Hearing Protection Earbuds

ISOtunesIT-21Qty: 3

$65/unit MSP cost (wholesale) / $99-$109 suggested resale

OSHA-compliant hearing protection (NRR 27 dB) with integrated Bluetooth microphone for clear voice dictation in loud environments. Superintendent wears these during active construction phases to narrate punch items while maintaining hearing safety compliance. Pairs with fleet phone or PLAUD recorder via Bluetooth.

Insta360 X4 Air 360° Camera

Insta360X4-AIR-STDQty: 1

$420/unit MSP cost (bundle with mount) / $549-$599 suggested resale configured

Optional Tier 3 visual documentation device for walkthroughs requiring photographic evidence. Mounts to hard hat via Insta360 Hard Hat Camera Mount (included in bundle). Captures 8K 360° video with audio narration simultaneously, providing visual proof of punch list items. Recommended for final walkthroughs, owner inspections, and dispute-prone projects.

Insta360 Hard Hat Camera Mount

Insta360CINSBBMGQty: 1

$35/unit MSP cost / included in X4 Air bundle or $49 standalone resale

Aluminum alloy magnetic quick-release mount with anti-vibration for attaching Insta360 X4 Air to standard hard hats. Industrial-grade construction for daily site use.

OtterBox Defender Series Case (Galaxy XCover7 Pro)

OtterBoxVaries by phone modelQty: 2

$35/unit MSP cost / $59-$69 suggested resale

Additional drop protection layer for fleet smartphones. While the XCover7 Pro is already rugged, the Defender adds a belt holster clip and screen protector for extended field durability. Matches quantity of fleet phones.

Software Procurement

WalkPunch

WalkPunchSaaS - Free tier with paid plansQty: 1

$0/month (free plan for pilot); paid plans TBD for volume

Purpose-built SaaS platform for the exact use case: upload walkthrough audio/video, AI generates trade-sorted punch list with transcript generation, issue extraction, trade classification, room inference, priority tagging, and sequence sorting. Exports PDF by trade. Lowest friction starting point for validating the workflow before building custom.

OpenAI API (Whisper + GPT-5.4 mini)

OpenAIWhisper + GPT-5.4 mini

Whisper: $0.006/min ($0.36/hr); GPT-5.4 mini: $0.15/1M input tokens + $0.60/1M output tokens. Estimated $0.20-$0.25 per 30-min walkthrough total.

Core AI engine for the custom pipeline. Whisper API transcribes field audio with excellent noise handling. GPT-5.4 mini processes transcripts to extract structured punch list items (location, trade, priority, description). Combined cost per walkthrough is under $0.25.

Deepgram Nova-2

DeepgramNova-2Qty: Usage-based API

$0.0043/min pre-recorded ($0.258/hr); $200 free credit to start

Alternative/backup transcription API with superior real-time streaming capability. Nova-2 offers competitive accuracy at lower cost than most competitors. $200 free credit covers approximately 775 hours of transcription—enough for months of piloting. Recommended as fallback if Whisper accuracy is insufficient for specific site noise profiles.

Fieldwire by Hilti

Hilti (Fieldwire)Per-seat SaaSQty: Free (5 users, 3 projects); Pro: per user/mo annual; Business: per user/mo annual

Free tier; Pro: $39/user/mo annual; Business: $59/user/mo annual

Field management platform for punch list tracking, assignment, and closeout after AI generates items. Provides plan markup, task management, photo annotation, and subcontractor assignment. Free tier covers pilot; Pro tier for production deployment. API available for automated punch item creation from AI pipeline.

Raken

RakenCore / Professional / EnterpriseQty: Per-seat SaaS

Core: $15/user/mo; Professional: $30/user/mo; Enterprise: $49/user/mo

Alternative to Fieldwire with native voice-to-text daily reporting capability. If client already uses Raken for daily logs, add punch list module rather than introducing a second platform. Strong mobile experience and affordable per-seat pricing.

Procore (existing client subscription)

Procore TechnologiesAnnual SaaS subscriptionQty: 1

$4,500-$10,000/yr for small firms (client's existing cost); MSP charges integration fee only

If client already subscribes to Procore, leverage its native Quick Capture voice-enabled punch list feature and REST API for direct punch item push from custom AI pipeline. Do not recommend new Procore subscriptions solely for this project due to cost.

Amazon S3 (Audio/Video Archive)

Amazon Web ServicesUsage-based cloud storage

~$0.023/GB/mo; estimated $1-$5/mo per client for audio archive

Cloud storage for raw audio files, transcripts, and generated punch list documents. Lifecycle policies auto-delete raw audio after 90 days per compliance requirements while retaining structured punch list data indefinitely.

AWS Lambda (Serverless Compute)

Amazon Web ServicesUsage-based serverless

~$0.20 per 1M requests + $0.0000166667/GB-second; estimated $5-$20/mo per client

Serverless compute for the transcription-to-punch-list processing pipeline. Runs Python functions triggered by audio file upload to S3. No servers to manage, scales to zero when idle, and handles burst processing during walkthrough uploads.

$6/user/mo (if not already subscribed)

SharePoint/OneDrive integration for punch list PDF delivery to client stakeholders. Teams channel notifications for new punch lists awaiting review. Leverages client's existing M365 tenant in most cases.

Otter.ai Business

Otter.aiPer-seat SaaS

$20/user/mo annual billing; 6,000 min/mo transcription

Optional all-in-one transcription platform for clients who want a simpler managed experience without custom API development. Less construction-specific but lower MSP technical burden. 6,000 minutes per user covers extensive daily use.

Prerequisites

  • Active cellular service (4G LTE minimum, 5G preferred) for field devices or a site WiFi network with minimum 10 Mbps upload bandwidth for audio/video file transfers
  • At least one designated field superintendent or project manager who performs regular site walkthroughs (minimum 2-3 per week to justify ROI)
  • Client must have an existing construction project management platform (Fieldwire, Procore, Buildertrend, Raken, or CoConstruct) OR be willing to adopt Fieldwire free tier for punch list tracking
  • Client must have a Google Workspace or Microsoft 365 tenant for email delivery of punch list reports and cloud file storage
  • MSP must have an OpenAI API account with billing configured and API key generated (https://platform.openai.com); minimum $50 prepaid credit recommended for pilot
  • MSP must have an AWS account with S3 and Lambda access configured, or equivalent serverless platform (Azure Functions + Blob Storage)
  • Legal review of state-specific audio recording consent laws completed for the client's operating jurisdiction(s)—critical for two-party consent states (CA, CT, DE, FL, IL, MD, MA, MI, MT, NV, NH, PA, WA)
  • Client must provide: (a) standard trade categories used on their projects, (b) typical location naming conventions (e.g., 'Unit 301 Kitchen' vs 'Building A Floor 3 Unit 1'), (c) priority classification scheme (e.g., Life Safety / High / Medium / Low / Cosmetic), (d) sample completed punch lists from 2-3 recent projects for AI prompt calibration
  • All field users must have personal or company-issued smartphones running iOS 16+ or Android 12+ with minimum 4GB RAM and 64GB storage
  • Site signage materials procured: weatherproof 'Audio/Video Recording in Progress' signs (minimum 8.5x11 inches, English and Spanish) for posting at site entry points
  • Subcontractor agreement addendum template prepared with recording consent language, reviewed by client's legal counsel
  • MDM solution (Microsoft Intune, Jamf, or Mosyle) configured if deploying MSP-managed fleet devices

Installation Steps

Before any hardware is deployed or recordings are made, establish the legal and compliance framework for audio recording on construction sites. This step is CRITICAL and must be completed first. Failure to address recording consent can expose the client to civil liability and criminal penalties in two-party consent states.

Note

In two-party consent states (CA, CT, DE, FL, IL, MD, MA, MI, MT, NV, NH, PA, WA), every person whose voice is captured must consent. The safest approach is 'self-narration mode' where the superintendent narrates observations to their own device without recording conversations with others—this qualifies as one-party consent even in strict states. If the client operates in multiple states, default to the strictest applicable standard.

1
Print and laminate weatherproof 'Audio/Video Recording in Progress' signs in English and Spanish.
2
Post at all site entry points, site office doors, and at the start of each floor/wing being documented.
3
Prepare subcontractor agreement addendum with recording consent clause and add to site orientation sign-in sheet.
4
Have client's legal counsel review all documents before first use.
Note

Template language for subcontractor addendum: 'Subcontractor acknowledges and consents that [Client Company] may conduct audio and video recording on the project site for documentation, quality assurance, and punch list generation purposes. Recordings will be processed by AI transcription services and retained per project documentation policies.' Adjust per legal counsel. Signs should reference both audio and video recording even if only audio is used initially.

Step 3: AWS Infrastructure Provisioning

Set up the cloud backend infrastructure for audio storage, processing pipeline, and punch list data storage. Create an S3 bucket for audio uploads with lifecycle policies, Lambda functions for processing orchestration, and a DynamoDB table for punch list item storage.

bash
aws s3 mb s3://client-name-walkthrough-audio --region us-east-1
aws s3api put-bucket-lifecycle-configuration --bucket client-name-walkthrough-audio --lifecycle-configuration '{"Rules":[{"ID":"DeleteRawAudioAfter90Days","Filter":{"Prefix":"raw-audio/"},"Status":"Enabled","Expiration":{"Days":90}},{"ID":"DeleteProcessedAudioAfter180Days","Filter":{"Prefix":"processed/"},"Status":"Enabled","Expiration":{"Days":180}}]}'
aws s3api put-bucket-encryption --bucket client-name-walkthrough-audio --server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
aws dynamodb create-table --table-name client-name-punch-items --attribute-definitions AttributeName=walkthrough_id,AttributeType=S AttributeName=item_id,AttributeType=S --key-schema AttributeName=walkthrough_id,KeyType=HASH AttributeName=item_id,KeyType=RANGE --billing-mode PAY_PER_REQUEST --region us-east-1
aws iam create-role --role-name walkthrough-processor-role --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
aws iam attach-role-policy --role-name walkthrough-processor-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Note

Replace 'client-name' with a slug of the client company name in all resource names. Enable S3 bucket versioning if the client requires audit trails. The 90-day lifecycle on raw audio balances compliance (retaining evidence through typical punch list closeout period) with data minimization (not retaining voice recordings indefinitely). Adjust retention periods per client's legal counsel recommendation.

Step 4: OpenAI API Configuration and Testing

Configure the OpenAI API account, set up organization-level usage limits to prevent runaway costs, and validate that the Whisper transcription and GPT-5.4 mini structured extraction APIs are functioning correctly with sample construction audio.

1
Install required Python packages
2
Set your OpenAI API key in a .env file
3
Test Whisper transcription with a sample audio file
Install dependencies and configure API key
bash
pip install openai boto3 python-dotenv
echo 'OPENAI_API_KEY=sk-your-api-key-here' > .env
Validate Whisper transcription API with a sample construction audio file
python
import openai, os
from dotenv import load_dotenv
load_dotenv()
client = openai.OpenAI()

# Test Whisper transcription with a sample file
with open('test_walkthrough.m4a', 'rb') as audio_file:
    transcript = client.audio.transcriptions.create(
        model='whisper-1',
        file=audio_file,
        response_format='verbose_json',
        timestamp_granularities=['segment']
    )
    print('Transcription successful:')
    print(transcript.text[:500])
Note

Set monthly usage limits in the OpenAI dashboard under Settings > Limits. Recommended starting limit: $50/month for pilot phase. A 30-minute walkthrough produces approximately 4,000-6,000 words of transcript (~5,000-7,500 tokens), costing approximately $0.18 for Whisper transcription and $0.01-$0.02 for GPT-5.4 mini extraction. Record a 2-3 minute test audio on a real construction site before proceeding—use the superintendent's actual narration style with background noise to validate accuracy.

Step 5: Deploy Transcription and Extraction Lambda Functions

Create and deploy the AWS Lambda functions that form the core processing pipeline: (1) an S3 trigger function that initiates transcription when audio is uploaded, (2) the transcription function that calls Whisper API, and (3) the extraction function that calls GPT-5.4 mini to parse the transcript into structured punch list items.

Create deployment package, deploy both Lambda functions, and configure S3 event notification trigger
bash
mkdir -p walkthrough-processor && cd walkthrough-processor
pip install openai boto3 -t ./package
cd package && zip -r ../deployment.zip . && cd ..
zip deployment.zip lambda_function.py
aws lambda create-function --function-name walkthrough-transcribe --runtime python3.12 --handler lambda_function.transcribe_handler --role arn:aws:iam::ACCOUNT_ID:role/walkthrough-processor-role --zip-file fileb://deployment.zip --timeout 300 --memory-size 512 --environment 'Variables={OPENAI_API_KEY=sk-your-key,PUNCH_TABLE=client-name-punch-items}'
aws lambda create-function --function-name walkthrough-extract --runtime python3.12 --handler lambda_function.extract_handler --role arn:aws:iam::ACCOUNT_ID:role/walkthrough-processor-role --zip-file fileb://deployment.zip --timeout 120 --memory-size 256 --environment 'Variables={OPENAI_API_KEY=sk-your-key,PUNCH_TABLE=client-name-punch-items}'
aws s3api put-bucket-notification-configuration --bucket client-name-walkthrough-audio --notification-configuration '{"LambdaFunctionConfigurations":[{"LambdaFunctionArn":"arn:aws:lambda:us-east-1:ACCOUNT_ID:function:walkthrough-transcribe","Events":["s3:ObjectCreated:*"],"Filter":{"Key":{"FilterRules":[{"Name":"prefix","Value":"raw-audio/"},{"Name":"suffix","Value":".m4a"}]}}}]}'
Note

Lambda timeout of 300 seconds (5 minutes) for transcription handles up to ~45 minutes of audio. For walkthroughs longer than 45 minutes, implement chunked processing in the Lambda function (split audio into 25MB segments per Whisper API limit). The extraction function has a 120-second timeout which is sufficient for processing even very long transcripts. Ensure the Lambda execution role has permissions for S3 read/write, DynamoDB write, and CloudWatch Logs.

Step 6: Hardware Preparation and MDM Enrollment

Unbox, configure, and enroll all field devices. For Samsung Galaxy XCover7 Pro fleet phones: perform initial setup, enroll in MDM, install required apps, configure recording profiles. For PLAUD Note Pro recorders: charge, pair with companion app, configure recording settings. For ISOtunes: pair with fleet phones via Bluetooth.

MDM Enrollment (Microsoft Intune)

1
In Intune admin center: Devices > Enrollment > Android > Android Enterprise
2
On device: Settings > Accounts > Add Work Account > enter user's M365 credentials
3
Install Company Portal app from managed Google Play
4
Push required apps via Intune: PLAUD companion app, Fieldwire, custom upload app

PLAUD Note Pro Setup

1
Power on device (hold power button 3 seconds)
2
Install PLAUD app on paired smartphone from App Store/Google Play
3
Enable Bluetooth on phone, open PLAUD app, tap 'Add Device'
4
Follow pairing prompts (device will vibrate when paired)
5
In PLAUD app: Settings > Recording Quality > set to 'High (48kHz)'
6
In PLAUD app: Settings > File Format > set to 'M4A'
7
Test recording: 30-second clip, verify playback clarity
Note

Order PLAUD Note Pro devices 2-3 weeks before planned deployment—shipping can take 7-10 business days. Charge all PLAUD devices fully before first deployment (USB-C, ~2 hours). Label each device with the assigned superintendent's name and a unique asset ID. Configure the PLAUD app to auto-sync recordings to the phone when Bluetooth-connected (Settings > Auto Sync > On). For the Samsung XCover7 Pro, enable the programmable XCover Key to launch the recording app with a single press for fastest field access.

Step 7: Build and Deploy Audio Upload Mobile App or Shortcut

Create a simple mechanism for field users to upload recorded audio from the PLAUD app or phone's native recorder to the S3 processing bucket. Options range from a lightweight custom app to an iOS Shortcut or Android Tasker automation that watches for new audio files and uploads them.

1
Option A: iOS Shortcut (simplest, no development required)
2
Open Shortcuts app on iPhone
3
Create new Shortcut: 'Upload Walkthrough'
4
Actions: 'Select File' (set to audio files, allow multiple), 'Get Details of File' > Name, 'Set Variable' filename, 'Get Contents of URL' (PUT request to pre-signed S3 URL), 'Show Notification' > 'Walkthrough uploaded - processing'
1
Option B: Python upload script for fleet phones (via Termux or custom APK)
upload_walkthrough.py
python
# Python upload script for fleet phones via Termux or custom APK

import boto3, sys, os, uuid
from datetime import datetime

s3 = boto3.client('s3',
    aws_access_key_id=os.environ['AWS_ACCESS_KEY'],
    aws_secret_access_key=os.environ['AWS_SECRET_KEY']
)

file_path = sys.argv[1]
project_id = sys.argv[2] if len(sys.argv) > 2 else 'default'
walkthrough_id = f"{project_id}/{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"

s3.upload_file(
    file_path,
    'client-name-walkthrough-audio',
    f'raw-audio/{walkthrough_id}.m4a',
    ExtraArgs={'Metadata': {'project': project_id, 'superintendent': os.environ.get('USER_NAME', 'unknown')}}
)
print(f'Uploaded: {walkthrough_id}')

Option C (Recommended): Simple web upload page hosted on S3 + CloudFront. See custom_ai_components for the full web upload interface implementation.

Note

Option C (web upload page) is recommended for most deployments because it works on any device without app installation, supports both iOS and Android, and can be bookmarked to the home screen as a PWA. The web page generates pre-signed S3 upload URLs server-side to avoid embedding AWS credentials on client devices. For the pilot phase, a simple email-to-S3 pipeline (superintendent emails audio file to a dedicated address that triggers processing) is the absolute lowest-friction option.

Step 8: Deploy Review and Approval Web Dashboard

Deploy the punch list review dashboard where superintendents and project managers review AI-generated punch list items before they are finalized and pushed to the PM platform. This is a lightweight web application hosted on S3/CloudFront (static frontend) with API Gateway + Lambda backend.

1
Deploy the static frontend to S3
2
Create API Gateway for backend
3
Create CloudFront distribution for HTTPS
Deploy the static frontend to S3
bash
aws s3 mb s3://client-name-punch-dashboard --region us-east-1
aws s3 website s3://client-name-punch-dashboard --index-document index.html --error-document error.html
aws s3 sync ./dashboard-frontend/ s3://client-name-punch-dashboard/ --acl public-read
Create API Gateway for backend
bash
aws apigateway create-rest-api --name 'PunchListAPI' --description 'Walkthrough punch list API' --endpoint-configuration '{"types":["REGIONAL"]}'
Create CloudFront distribution for HTTPS
bash
aws cloudfront create-distribution --origin-domain-name client-name-punch-dashboard.s3-website-us-east-1.amazonaws.com --default-root-object index.html
Note

The dashboard is intentionally simple—construction superintendents need a mobile-friendly interface they can use on-site with one hand. Key features: list of walkthroughs with status (Processing / Ready for Review / Approved / Exported), expandable punch item list with edit capability, bulk approve/reject, export to PDF grouped by trade, and push to Fieldwire/Procore. Use responsive CSS and large touch targets (minimum 44x44px per WCAG). Consider password-protecting with Amazon Cognito or a simple shared access code for the pilot phase.

Step 9: Configure Fieldwire Integration for Punch List Push

Set up the integration between the AI processing pipeline and Fieldwire so that approved punch list items are automatically created as tasks in the appropriate Fieldwire project. This uses the Fieldwire REST API to create tasks with location, assignee, priority, and category fields populated from the AI extraction.

1
Install Fieldwire API client
2
Test Fieldwire API connectivity
3
Test creating a punch list task
Install Fieldwire API client
bash
pip install requests
Test Fieldwire API connectivity
python
# list projects to verify access

import requests

FIELDWIRE_API_TOKEN = 'your-fieldwire-api-token'
headers = {
    'Authorization': f'Bearer {FIELDWIRE_API_TOKEN}',
    'Content-Type': 'application/json'
}

# List projects to verify access
response = requests.get('https://clientapi.fieldwire.com/api/v3/projects', headers=headers)
print(f'Status: {response.status_code}')
for project in response.json():
    print(f'Project: {project["name"]} (ID: {project["id"]})')
Test creating a punch list task in Fieldwire
python
import requests, json

FIELDWIRE_API_TOKEN = 'your-fieldwire-api-token'
PROJECT_ID = 'your-project-id'
headers = {
    'Authorization': f'Bearer {FIELDWIRE_API_TOKEN}',
    'Content-Type': 'application/json'
}

task_data = {
    'name': 'TEST - Kitchen faucet leaking at supply valve',
    'priority': 2,
    'status_id': 0,
    'creator_user_id': None,
    'location_id': None,
    'labels': ['Plumbing', 'Punch List', 'AI-Generated']
}

response = requests.post(
    f'https://clientapi.fieldwire.com/api/v3/projects/{PROJECT_ID}/tasks',
    headers=headers,
    json=task_data
)
print(f'Created task: {response.json()["id"]}')
Note

Fieldwire API tokens are generated in the Fieldwire web app under Account Settings > API. The API is well-documented at https://developers.fieldwire.com. For clients using Procore instead, use the Procore REST API (https://developers.procore.com) with OAuth2 authentication—the punch item creation endpoint is POST /rest/v1.0/projects/{project_id}/punch_items. For clients without either platform, the PDF export (grouped by trade, with location and priority) is the primary deliverable. Map the AI extraction trade categories to the client's Fieldwire category list during setup.

Step 10: Configure Procore Integration (If Applicable)

For clients using Procore, configure OAuth2 authentication and test the punch item creation API. Procore's REST API allows direct creation of punch items with assignee, location, trade, priority, and due date fields. This step is only needed if the client uses Procore; skip if using Fieldwire or PDF-only export.

1
Register a Procore Developer App at https://developers.procore.com
2
Create new app: 'Walkthrough Punch List AI'
3
Set redirect URI to your dashboard URL + '/oauth/callback'
4
Note Client ID and Client Secret
5
Request access to Punch Items tool
Test OAuth2 flow and create a test punch item via Procore API
python
python3 -c "
import requests

CLIENT_ID = 'your-procore-client-id'
CLIENT_SECRET = 'your-procore-client-secret'
REDIRECT_URI = 'https://your-dashboard-url/oauth/callback'
AUTH_CODE = 'code-from-oauth-redirect'

# Exchange auth code for access token
token_response = requests.post('https://login.procore.com/oauth/token', data={
    'grant_type': 'authorization_code',
    'code': AUTH_CODE,
    'client_id': CLIENT_ID,
    'client_secret': CLIENT_SECRET,
    'redirect_uri': REDIRECT_URI
})

 access_token = token_response.json()['access_token']
print(f'Access token obtained: {access_token[:20]}...')

# Create test punch item
headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json'}
company_id = 'your-company-id'
project_id = 'your-project-id'

punch_item = {
    'punch_item': {
        'name': 'TEST - Kitchen faucet leaking at supply valve',
        'description': 'AI-generated from walkthrough transcription. Superintendent noted active leak at supply valve under kitchen sink, third unit from left.',
        'priority': 'high',
        'punch_item_type': 'Plumbing',
        'location_id': None,
        'assignee_id': None
    }
}

response = requests.post(
    f'https://app.procore.com/rest/v1.0/projects/{project_id}/punch_items',
    headers=headers,
    json=punch_item
)
print(f'Created Procore punch item: {response.status_code}')
"
Note

Procore API rate limits are 3,600 requests per hour per app. A typical walkthrough generating 20-50 punch items is well within limits. Procore OAuth tokens expire after 2 hours—implement refresh token logic in production. The Procore Developer App must be approved by Procore for production use (sandbox is available immediately for testing). Allow 1-2 weeks for Procore app approval.

Step 11: Pilot Deployment with Single Superintendent

Deploy the complete system to one superintendent for a 2-week pilot. Provide hardware (PLAUD Note Pro + ISOtunes), train on narration technique, process 4-6 real walkthroughs, and evaluate accuracy. This step validates the entire pipeline before broader rollout.

Note

Critical training for the superintendent: Teach structured narration technique for optimal AI extraction. Example narration pattern: '[Location] - [Trade] - [Priority] - [Description]'. Sample: 'Unit 301, Master Bathroom. Trade: Plumbing. Priority: High. Hot water supply line has active drip at shut-off valve below vanity. Needs replacement before drywall close.' Pause 2-3 seconds between items. Speak clearly but naturally—the AI handles background noise well. Avoid narrating while directly next to active power tools. Clip PLAUD Note Pro to hard hat strap at ear level for best capture. Start and stop recording at building entry/exit. The first 2-3 walkthroughs will require significant manual review and AI prompt tuning—this is expected and normal.

Step 12: AI Prompt Calibration and Accuracy Tuning

After 4-6 pilot walkthroughs, review AI extraction accuracy against the superintendent's manual corrections. Tune the GPT-5.4 mini system prompt to improve trade classification, location parsing, and priority assignment based on the client's specific terminology and project conventions. This is the most impactful step for system quality.

Run accuracy analysis on pilot walkthroughs
python
python3 -c "
import json

# Load AI-generated items and superintendent corrections
with open('pilot_results.json') as f:
    results = json.load(f)

total_items = 0
correct_trade = 0
correct_location = 0
correct_priority = 0
missed_items = 0
false_items = 0

for walkthrough in results['walkthroughs']:
    for item in walkthrough['items']:
        total_items += 1
        if item['ai_trade'] == item['corrected_trade']:
            correct_trade += 1
        if item['ai_location'] == item['corrected_location']:
            correct_location += 1
        if item['ai_priority'] == item['corrected_priority']:
            correct_priority += 1
    missed_items += walkthrough.get('missed_count', 0)
    false_items += walkthrough.get('false_positive_count', 0)

print(f'Total items: {total_items}')
print(f'Trade accuracy: {correct_trade/total_items*100:.1f}%')
print(f'Location accuracy: {correct_location/total_items*100:.1f}%')
print(f'Priority accuracy: {correct_priority/total_items*100:.1f}%')
print(f'Missed items: {missed_items}')
print(f'False positives: {false_items}')
print(f'Precision: {total_items/(total_items+false_items)*100:.1f}%')
print(f'Recall: {total_items/(total_items+missed_items)*100:.1f}%')
"
Note

Target accuracy thresholds for production readiness: Trade classification >85%, Location parsing >80%, Priority assignment >75%, Recall (no missed items) >90%. The most common calibration adjustments are: (1) adding client-specific trade names to the prompt (e.g., 'HVAC' vs 'Mechanical'), (2) teaching location hierarchy (Building > Floor > Unit > Room), (3) adjusting priority thresholds to match client's standards. If accuracy is below 70% on any dimension after tuning, the narration technique likely needs coaching rather than prompt changes. See custom_ai_components for the full prompt template with calibration notes.

Step 13: Production Rollout to Full Field Team

After pilot validation, expand deployment to all field superintendents. Distribute hardware, conduct group training, set up per-project configurations, and establish the ongoing review workflow. Typically 3-10 superintendents for a mid-size contractor.

Note

Stagger rollout to 2-3 superintendents per week to manage training load and catch issues early. Create a 1-page laminated quick reference card for each superintendent covering: (1) How to start/stop recording, (2) Narration technique with examples, (3) How to upload audio, (4) How to review and approve punch items, (5) Who to call if something isn't working. Schedule a 30-minute group training session plus 15-minute individual ride-alongs with each super on their first real walkthrough. The project manager or office admin should be trained on the review dashboard and export process separately.

Step 14: PDF Export and Report Template Configuration

Configure the punch list PDF export template to match the client's branding and contractual requirements. Most owners and GCs have specific expectations for punch list format. The export groups items by trade, includes location, priority, description, and optionally a timestamp or photo reference.

Note

Use a PDF generation library like WeasyPrint (Python) or Puppeteer (Node.js) to render HTML templates to PDF. Include client logo, project name, date, superintendent name, and a sign-off line for GC/owner review. Standard grouping order: by Trade (alphabetical), then by Location (building > floor > unit > room), then by Priority (descending). Include item count summary at top: 'Total: 47 items | Plumbing: 12 | Electrical: 8 | Drywall: 15 | Paint: 7 | Flooring: 5'. Add a 'Generated by AI transcription — reviewed and approved by [Superintendent Name] on [Date]' disclaimer footer per legal best practice.

Custom AI Components

Walkthrough Audio Transcription Function

Type: skill AWS Lambda function that receives an S3 event notification when a new audio file is uploaded to the raw-audio/ prefix, downloads the file, sends it to OpenAI Whisper API for transcription, stores the transcript, and triggers the extraction function. Handles audio files up to 25MB (Whisper API limit) and automatically chunks larger files.

Implementation:

python
import json
import os
import boto3
import openai
import tempfile
import uuid
from datetime import datetime

s3 = boto3.client('s3')
dynamodb = boto3.resource('dynamodb')
lambda_client = boto3.client('lambda')

OPENAI_API_KEY = os.environ['OPENAI_API_KEY']
PUNCH_TABLE = os.environ['PUNCH_TABLE']
BUCKET_NAME = os.environ.get('BUCKET_NAME', 'client-name-walkthrough-audio')

client = openai.OpenAI(api_key=OPENAI_API_KEY)

def transcribe_handler(event, context):
    """Triggered by S3 ObjectCreated event on raw-audio/ prefix."""
    
    record = event['Records'][0]
    bucket = record['s3']['bucket']['name']
    key = record['s3']['object']['key']
    
    # Extract metadata
    head_response = s3.head_object(Bucket=bucket, Key=key)
    metadata = head_response.get('Metadata', {})
    project_id = metadata.get('project', 'unknown')
    superintendent = metadata.get('superintendent', 'unknown')
    
    # Generate walkthrough ID
    walkthrough_id = key.replace('raw-audio/', '').replace('.m4a', '').replace('.mp3', '').replace('.wav', '')
    
    # Download audio to temp file
    with tempfile.NamedTemporaryFile(suffix='.m4a', delete=False) as tmp:
        s3.download_file(bucket, key, tmp.name)
        tmp_path = tmp.name
    
    try:
        # Check file size - Whisper API limit is 25MB
        file_size = os.path.getsize(tmp_path)
        
        if file_size > 25 * 1024 * 1024:
            # For files > 25MB, use chunked approach
            transcript_text = _transcribe_chunked(tmp_path)
        else:
            # Standard transcription
            with open(tmp_path, 'rb') as audio_file:
                transcript_response = client.audio.transcriptions.create(
                    model='whisper-1',
                    file=audio_file,
                    response_format='verbose_json',
                    timestamp_granularities=['segment'],
                    language='en',
                    prompt='Construction site walkthrough punch list narration. Trades include plumbing, electrical, HVAC, drywall, paint, flooring, carpentry, roofing, concrete, steel, fire protection, insulation, landscaping, doors, windows, hardware, appliances, countertops, cabinets, tile.'
                )
                transcript_text = transcript_response.text
                segments = transcript_response.segments if hasattr(transcript_response, 'segments') else []
        
        # Store transcript in S3
        transcript_key = f'transcripts/{walkthrough_id}.json'
        transcript_data = {
            'walkthrough_id': walkthrough_id,
            'project_id': project_id,
            'superintendent': superintendent,
            'timestamp': datetime.utcnow().isoformat(),
            'audio_key': key,
            'transcript': transcript_text,
            'segments': [{'start': s.start, 'end': s.end, 'text': s.text} for s in segments] if segments else [],
            'audio_duration_seconds': segments[-1].end if segments else None,
            'status': 'transcribed'
        }
        
        s3.put_object(
            Bucket=bucket,
            Key=transcript_key,
            Body=json.dumps(transcript_data, indent=2),
            ContentType='application/json'
        )
        
        # Trigger extraction function
        lambda_client.invoke(
            FunctionName='walkthrough-extract',
            InvocationType='Event',  # Async
            Payload=json.dumps({
                'walkthrough_id': walkthrough_id,
                'transcript_key': transcript_key,
                'bucket': bucket,
                'project_id': project_id,
                'superintendent': superintendent
            })
        )
        
        return {
            'statusCode': 200,
            'body': json.dumps({
                'walkthrough_id': walkthrough_id,
                'transcript_length': len(transcript_text),
                'status': 'transcribed_and_extraction_triggered'
            })
        }
        
    finally:
        os.unlink(tmp_path)


def _transcribe_chunked(file_path):
    """Handle files larger than 25MB by splitting with pydub."""
    from pydub import AudioSegment
    
    audio = AudioSegment.from_file(file_path)
    chunk_length_ms = 10 * 60 * 1000  # 10-minute chunks
    chunks = [audio[i:i + chunk_length_ms] for i in range(0, len(audio), chunk_length_ms)]
    
    full_transcript = []
    for i, chunk in enumerate(chunks):
        with tempfile.NamedTemporaryFile(suffix='.m4a', delete=False) as tmp:
            chunk.export(tmp.name, format='ipod')  # M4A format
            with open(tmp.name, 'rb') as chunk_file:
                response = client.audio.transcriptions.create(
                    model='whisper-1',
                    file=chunk_file,
                    response_format='text',
                    language='en',
                    prompt='Construction site walkthrough punch list narration. Continuing from previous segment.'
                )
                full_transcript.append(response)
            os.unlink(tmp.name)
    
    return ' '.join(full_transcript)

Punch List Item Extraction Prompt

Type: prompt GPT-5.4 mini system prompt that takes a raw walkthrough transcript and extracts structured punch list items with location, trade, priority, description, and optional notes. Includes construction-specific trade taxonomy, priority classification guidance, and output schema. This prompt is the core intelligence of the system and should be calibrated per client during the pilot phase.

Implementation:

System Prompt
text
SYSTEM PROMPT:
---
You are a construction punch list extraction AI. You analyze transcripts of superintendent walkthrough narrations and extract individual punch list items.

For each issue or deficiency mentioned in the transcript, extract a structured punch list item.

## Output Format
Respond with valid JSON only. No markdown, no explanation. Use this exact schema:

{
  "walkthrough_summary": "Brief 1-2 sentence summary of the walkthrough scope",
  "total_items": <integer>,
  "items": [
    {
      "item_number": <sequential integer starting at 1>,
      "location": {
        "building": "<building name/number or null>",
        "floor": "<floor number/name or null>",
        "unit": "<unit number or null>",
        "room": "<room name: Kitchen, Master Bathroom, Hallway, etc.>",
        "detail": "<specific location detail: 'north wall', 'above entry door', 'third from left', etc.>"
      },
      "trade": "<one of the standard trades listed below>",
      "priority": "<one of: Life Safety | High | Medium | Low | Cosmetic>",
      "description": "<clear, actionable description of the deficiency>",
      "action_required": "<specific corrective action: Replace, Repair, Adjust, Clean, Install, Complete, Touch-up, etc.>",
      "notes": "<any additional context from the narration, or null>",
      "confidence": <float 0.0-1.0 indicating extraction confidence>
    }
  ],
  "ambiguous_mentions": [
    {
      "transcript_excerpt": "<the unclear portion of transcript>",
      "reason": "<why this couldn't be parsed as a punch item>"
    }
  ]
}

## Standard Trade Categories
Use EXACTLY one of these trade names (map superintendent's informal language to the closest match):
- Plumbing
- Electrical
- HVAC
- Drywall
- Paint
- Flooring
- Carpentry / Millwork
- Roofing
- Concrete
- Structural Steel
- Fire Protection
- Insulation
- Doors / Frames / Hardware
- Windows / Glazing
- Appliances
- Cabinets / Countertops
- Tile / Stone
- Landscaping / Sitework
- Elevator
- Low Voltage / AV
- Cleaning
- General / Other

## Priority Classification Guide
- **Life Safety**: Fire code violations, missing egress signage, exposed wiring, trip hazards in common areas, missing handrails, fire suppression deficiencies
- **High**: Active water leaks, HVAC not functioning, no hot water, broken locks, code violations that block inspection sign-off
- **Medium**: Cosmetic drywall damage visible at eye level, misaligned doors, improper caulking, fixture not level, minor plumbing drip
- **Low**: Minor paint touch-ups, small nail pops, slight grout discoloration, adjustable hardware items
- **Cosmetic**: Scuffs, cleaning items, protective film removal, minor blemishes only visible upon close inspection

## Extraction Rules
1. Each distinct deficiency is a separate item, even if mentioned in the same sentence
2. If the superintendent mentions location once and then lists multiple issues, apply that location to all subsequent items until a new location is stated
3. If trade is not explicitly stated, infer from the description (e.g., 'leaking faucet' = Plumbing)
4. If priority is not explicitly stated, infer from the severity using the classification guide above
5. Convert informal language to professional descriptions (e.g., 'this thing is messed up' → describe the actual deficiency based on context)
6. Ignore non-punch-list narration (greetings, phone calls, casual conversation, project status updates)
7. If the superintendent says 'same as before' or 'same issue', create a new item with the previous item's trade and description but the current location
8. Flag items with confidence < 0.6 — these need superintendent review
9. Preserve any specific measurements, counts, or unit identifiers mentioned ('third unit from left', '6-inch gap', 'both bathrooms')
10. If a single mention implies multiple items (e.g., 'all six units need paint touch-up in kitchen'), create one item with the quantity noted in the description

## Client-Specific Customization
<!-- MSP: Replace the section below with client-specific terms during pilot calibration -->
- Location hierarchy: [Building Name/Number] > [Floor] > [Unit Number] > [Room]
- Client calls HVAC: "mechanical" — map to HVAC
- Client uses priority: "urgent" — map to High
- Client uses priority: "whenever" or "low priority" — map to Low
- Common abbreviations: "MBR" = Master Bedroom, "MBA" = Master Bathroom, "LR" = Living Room, "KIT" = Kitchen, "GR" = Great Room
---
User Prompt
text
USER PROMPT:
---
Extract all punch list items from the following construction walkthrough transcript.

Project: {{project_name}}
Date: {{walkthrough_date}}
Superintendent: {{superintendent_name}}

Transcript:
{{transcript_text}}
---

Punch List Extraction Lambda Function

Type: skill AWS Lambda function that receives the walkthrough transcript, sends it to GPT-5.4 mini with the extraction prompt, validates the structured response, stores punch list items in DynamoDB, and updates the walkthrough status. Includes retry logic and error handling for malformed AI responses.

Implementation

Punch List Extraction Lambda — extract_handler
python
import json
import os
import boto3
import openai
from datetime import datetime
from decimal import Decimal

s3 = boto3.client('s3')
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['PUNCH_TABLE'])

OPENAI_API_KEY = os.environ['OPENAI_API_KEY']
client = openai.OpenAI(api_key=OPENAI_API_KEY)

# Load the system prompt from environment or S3
SYSTEM_PROMPT_KEY = os.environ.get('SYSTEM_PROMPT_KEY', 'config/extraction_prompt.txt')

VALID_TRADES = [
    'Plumbing', 'Electrical', 'HVAC', 'Drywall', 'Paint', 'Flooring',
    'Carpentry / Millwork', 'Roofing', 'Concrete', 'Structural Steel',
    'Fire Protection', 'Insulation', 'Doors / Frames / Hardware',
    'Windows / Glazing', 'Appliances', 'Cabinets / Countertops',
    'Tile / Stone', 'Landscaping / Sitework', 'Elevator',
    'Low Voltage / AV', 'Cleaning', 'General / Other'
]

VALID_PRIORITIES = ['Life Safety', 'High', 'Medium', 'Low', 'Cosmetic']


def extract_handler(event, context):
    """Process transcript and extract structured punch list items."""
    
    walkthrough_id = event['walkthrough_id']
    transcript_key = event['transcript_key']
    bucket = event['bucket']
    project_id = event.get('project_id', 'unknown')
    superintendent = event.get('superintendent', 'unknown')
    
    # Load transcript
    transcript_obj = s3.get_object(Bucket=bucket, Key=transcript_key)
    transcript_data = json.loads(transcript_obj['Body'].read().decode('utf-8'))
    transcript_text = transcript_data['transcript']
    
    # Load system prompt
    try:
        prompt_obj = s3.get_object(Bucket=bucket, Key=SYSTEM_PROMPT_KEY)
        system_prompt = prompt_obj['Body'].read().decode('utf-8')
    except Exception:
        system_prompt = DEFAULT_SYSTEM_PROMPT  # Fallback to hardcoded prompt
    
    # Build user message
    user_message = f"""Extract all punch list items from the following construction walkthrough transcript.

Project: {project_id}
Date: {datetime.utcnow().strftime('%Y-%m-%d')}
Superintendent: {superintendent}

Transcript:
{transcript_text}"""
    
    # Call GPT-5.4 mini with retry logic
    max_retries = 3
    extracted_data = None
    
    for attempt in range(max_retries):
        try:
            response = client.chat.completions.create(
                model='gpt-5.4-mini',
                messages=[
                    {'role': 'system', 'content': system_prompt},
                    {'role': 'user', 'content': user_message}
                ],
                temperature=0.1,  # Low temperature for consistent extraction
                max_tokens=8000,
                response_format={'type': 'json_object'}
            )
            
            raw_response = response.choices[0].message.content
            extracted_data = json.loads(raw_response)
            
            # Validate structure
            if 'items' not in extracted_data:
                raise ValueError('Response missing "items" key')
            
            # Validate and clean each item
            for item in extracted_data['items']:
                if item.get('trade') not in VALID_TRADES:
                    item['trade'] = 'General / Other'
                    item['notes'] = (item.get('notes') or '') + f' [Original trade: {item.get("trade", "unknown")}]'
                if item.get('priority') not in VALID_PRIORITIES:
                    item['priority'] = 'Medium'
                item['confidence'] = item.get('confidence', 0.8)
            
            break  # Success
            
        except (json.JSONDecodeError, ValueError, KeyError) as e:
            if attempt < max_retries - 1:
                continue
            else:
                extracted_data = {
                    'walkthrough_summary': 'Extraction failed - manual review required',
                    'total_items': 0,
                    'items': [],
                    'error': str(e),
                    'raw_response': raw_response if 'raw_response' in dir() else None
                }
    
    # Store items in DynamoDB
    timestamp = datetime.utcnow().isoformat()
    
    with table.batch_writer() as batch:
        # Write walkthrough metadata
        batch.put_item(Item={
            'walkthrough_id': walkthrough_id,
            'item_id': 'METADATA',
            'project_id': project_id,
            'superintendent': superintendent,
            'timestamp': timestamp,
            'summary': extracted_data.get('walkthrough_summary', ''),
            'total_items': extracted_data.get('total_items', 0),
            'status': 'pending_review',
            'ambiguous_mentions': json.dumps(extracted_data.get('ambiguous_mentions', []))
        })
        
        # Write each punch item
        for item in extracted_data.get('items', []):
            item_id = f"ITEM#{item['item_number']:04d}"
            batch.put_item(Item={
                'walkthrough_id': walkthrough_id,
                'item_id': item_id,
                'item_number': item['item_number'],
                'location': json.dumps(item.get('location', {})),
                'trade': item['trade'],
                'priority': item['priority'],
                'description': item['description'],
                'action_required': item.get('action_required', 'Review'),
                'notes': item.get('notes'),
                'confidence': Decimal(str(item.get('confidence', 0.8))),
                'status': 'pending_review',
                'approved': False,
                'created_at': timestamp,
                'updated_at': timestamp
            })
    
    # Store full extraction result in S3
    extraction_key = f'extractions/{walkthrough_id}.json'
    s3.put_object(
        Bucket=bucket,
        Key=extraction_key,
        Body=json.dumps({
            'walkthrough_id': walkthrough_id,
            'project_id': project_id,
            'superintendent': superintendent,
            'extraction_timestamp': timestamp,
            'extracted_data': extracted_data,
            'model': 'gpt-5.4-mini',
            'prompt_version': '1.0'
        }, indent=2, default=str),
        ContentType='application/json'
    )
    
    # Update transcript status
    transcript_data['status'] = 'extracted'
    transcript_data['extraction_key'] = extraction_key
    transcript_data['item_count'] = len(extracted_data.get('items', []))
    s3.put_object(
        Bucket=bucket,
        Key=transcript_key,
        Body=json.dumps(transcript_data, indent=2),
        ContentType='application/json'
    )
    
    return {
        'statusCode': 200,
        'body': json.dumps({
            'walkthrough_id': walkthrough_id,
            'items_extracted': len(extracted_data.get('items', [])),
            'ambiguous_mentions': len(extracted_data.get('ambiguous_mentions', [])),
            'status': 'pending_review'
        })
    }


DEFAULT_SYSTEM_PROMPT = """You are a construction punch list extraction AI. You analyze transcripts of superintendent walkthrough narrations and extract individual punch list items. Respond with valid JSON containing a 'walkthrough_summary', 'total_items' count, 'items' array with item_number, location (building/floor/unit/room/detail), trade, priority (Life Safety/High/Medium/Low/Cosmetic), description, action_required, notes, and confidence score, plus an 'ambiguous_mentions' array for unclear portions."""

Fieldwire Push Integration

Type: integration Integration module that takes approved punch list items from DynamoDB and creates corresponding tasks in Fieldwire via the REST API. Maps AI-extracted fields to Fieldwire task properties including name, priority, category (trade), and labels. Supports batch creation of multiple items from a single walkthrough.

Implementation:

python
import json
import os
import requests
import boto3
from datetime import datetime

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['PUNCH_TABLE'])

FIELDWIRE_API_TOKEN = os.environ['FIELDWIRE_API_TOKEN']
FIELDWIRE_BASE_URL = 'https://clientapi.fieldwire.com/api/v3'

# Priority mapping: AI priority -> Fieldwire priority (1=High, 2=Normal, 3=Low)
PRIORITY_MAP = {
    'Life Safety': 1,
    'High': 1,
    'Medium': 2,
    'Low': 3,
    'Cosmetic': 3
}

def push_to_fieldwire(event, context):
    """Push approved punch list items to Fieldwire as tasks."""
    
    walkthrough_id = event['walkthrough_id']
    project_id_fieldwire = event['fieldwire_project_id']
    
    headers = {
        'Authorization': f'Bearer {FIELDWIRE_API_TOKEN}',
        'Content-Type': 'application/json'
    }
    
    # Query approved items from DynamoDB
    response = table.query(
        KeyConditionExpression='walkthrough_id = :wid',
        FilterExpression='approved = :approved AND begins_with(item_id, :prefix)',
        ExpressionAttributeValues={
            ':wid': walkthrough_id,
            ':approved': True,
            ':prefix': 'ITEM#'
        }
    )
    
    items = response['Items']
    created_tasks = []
    errors = []
    
    # Get existing categories (trades) in the Fieldwire project
    categories_response = requests.get(
        f'{FIELDWIRE_BASE_URL}/projects/{project_id_fieldwire}/categories',
        headers=headers
    )
    existing_categories = {c['name']: c['id'] for c in categories_response.json()}
    
    for item in items:
        location = json.loads(item.get('location', '{}'))
        
        # Build task name: [Priority] Location - Description
        location_parts = [v for v in [location.get('building'), location.get('floor'), 
                                       location.get('unit'), location.get('room'),
                                       location.get('detail')] if v]
        location_str = ', '.join(location_parts) if location_parts else 'Location TBD'
        
        task_name = f"[{item['priority']}] {location_str} - {item['description'][:100]}"
        
        # Ensure trade category exists
        trade = item.get('trade', 'General / Other')
        if trade not in existing_categories:
            cat_response = requests.post(
                f'{FIELDWIRE_BASE_URL}/projects/{project_id_fieldwire}/categories',
                headers=headers,
                json={'name': trade, 'color': '#FF6B35'}  # Orange for punch items
            )
            if cat_response.status_code == 200:
                existing_categories[trade] = cat_response.json()['id']
        
        task_data = {
            'name': task_name,
            'priority': PRIORITY_MAP.get(item.get('priority', 'Medium'), 2),
            'status_id': 0,  # 0 = Open
            'category_id': existing_categories.get(trade),
            'labels': ['Punch List', 'AI-Generated', item.get('priority', 'Medium')],
            'description': (
                f"Trade: {trade}\n"
                f"Location: {location_str}\n"
                f"Priority: {item.get('priority', 'Medium')}\n"
                f"Action Required: {item.get('action_required', 'Review')}\n"
                f"Description: {item.get('description', '')}\n"
                f"{'Notes: ' + item.get('notes') if item.get('notes') else ''}\n"
                f"---\n"
                f"AI Confidence: {float(item.get('confidence', 0)):.0%}\n"
                f"Source: Walkthrough {walkthrough_id}\n"
                f"Generated: {item.get('created_at', '')}"
            )
        }
        
        try:
            task_response = requests.post(
                f'{FIELDWIRE_BASE_URL}/projects/{project_id_fieldwire}/tasks',
                headers=headers,
                json=task_data
            )
            
            if task_response.status_code in [200, 201]:
                fieldwire_task_id = task_response.json()['id']
                created_tasks.append({
                    'item_id': item['item_id'],
                    'fieldwire_task_id': fieldwire_task_id
                })
                
                # Update DynamoDB with Fieldwire task ID
                table.update_item(
                    Key={'walkthrough_id': walkthrough_id, 'item_id': item['item_id']},
                    UpdateExpression='SET fieldwire_task_id = :tid, pushed_at = :ts',
                    ExpressionAttributeValues={
                        ':tid': fieldwire_task_id,
                        ':ts': datetime.utcnow().isoformat()
                    }
                )
            else:
                errors.append({
                    'item_id': item['item_id'],
                    'error': task_response.text,
                    'status_code': task_response.status_code
                })
                
        except Exception as e:
            errors.append({'item_id': item['item_id'], 'error': str(e)})
    
    return {
        'statusCode': 200,
        'body': json.dumps({
            'walkthrough_id': walkthrough_id,
            'tasks_created': len(created_tasks),
            'errors': len(errors),
            'created_tasks': created_tasks,
            'error_details': errors
        })
    }

PDF Punch List Report Generator

Type: skill Generates a professional PDF punch list report from approved items, grouped by trade with location, priority, and description. Includes client branding, summary statistics, and a review sign-off section. Uses WeasyPrint for HTML-to-PDF conversion.

Implementation

AWS Lambda handler that queries DynamoDB for approved punch list items, builds an HTML report grouped by trade, converts it to PDF via WeasyPrint, uploads to S3, and returns a 7-day pre-signed download URL.
python
import json
import os
import boto3
from datetime import datetime
from weasyprint import HTML
from collections import defaultdict

s3 = boto3.client('s3')
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['PUNCH_TABLE'])

def generate_pdf_handler(event, context):
    """Generate PDF punch list report from approved items."""
    
    walkthrough_id = event['walkthrough_id']
    client_name = event.get('client_name', 'Construction Client')
    project_name = event.get('project_name', 'Project')
    
    # Query all items
    response = table.query(
        KeyConditionExpression='walkthrough_id = :wid',
        ExpressionAttributeValues={':wid': walkthrough_id}
    )
    
    metadata = None
    items = []
    for record in response['Items']:
        if record['item_id'] == 'METADATA':
            metadata = record
        elif record['item_id'].startswith('ITEM#'):
            items.append(record)
    
    # Group by trade
    by_trade = defaultdict(list)
    for item in sorted(items, key=lambda x: x.get('item_number', 0)):
        by_trade[item.get('trade', 'General / Other')].append(item)
    
    # Priority counts
    priority_counts = defaultdict(int)
    for item in items:
        priority_counts[item.get('priority', 'Medium')] += 1
    
    # Build HTML
    trade_sections = ''
    for trade in sorted(by_trade.keys()):
        trade_items = by_trade[trade]
        rows = ''
        for item in trade_items:
            location = json.loads(item.get('location', '{}'))
            loc_parts = [v for v in [location.get('unit'), location.get('room'), location.get('detail')] if v]
            loc_str = ', '.join(loc_parts) if loc_parts else 'TBD'
            priority = item.get('priority', 'Medium')
            priority_class = priority.lower().replace(' ', '-')
            rows += f"""
            <tr>
                <td>{item.get('item_number', '')}</td>
                <td>{loc_str}</td>
                <td class="priority-{priority_class}">{priority}</td>
                <td>{item.get('description', '')}</td>
                <td>{item.get('action_required', '')}</td>
                <td class="status-cell">☐ Open</td>
            </tr>"""
        
        trade_sections += f"""
        <div class="trade-section">
            <h2>{trade} ({len(trade_items)} items)</h2>
            <table>
                <thead>
                    <tr><th>#</th><th>Location</th><th>Priority</th><th>Description</th><th>Action</th><th>Status</th></tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        </div>"""
    
    html_content = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <style>
            body {{ font-family: Arial, sans-serif; font-size: 10pt; margin: 20mm; }}
            h1 {{ color: #1a1a2e; border-bottom: 3px solid #e94560; padding-bottom: 10px; }}
            h2 {{ color: #1a1a2e; background: #f0f0f0; padding: 8px 12px; margin-top: 20px; }}
            table {{ width: 100%; border-collapse: collapse; margin-bottom: 15px; }}
            th {{ background: #1a1a2e; color: white; padding: 6px 8px; text-align: left; font-size: 9pt; }}
            td {{ border: 1px solid #ddd; padding: 5px 8px; font-size: 9pt; vertical-align: top; }}
            tr:nth-child(even) {{ background: #f9f9f9; }}
            .summary-box {{ background: #f0f4f8; border: 1px solid #d0d8e0; padding: 15px; margin: 15px 0; }}
            .summary-box span {{ margin-right: 20px; font-weight: bold; }}
            .priority-life-safety {{ color: #d32f2f; font-weight: bold; }}
            .priority-high {{ color: #e65100; font-weight: bold; }}
            .priority-medium {{ color: #f57f17; }}
            .priority-low {{ color: #2e7d32; }}
            .priority-cosmetic {{ color: #757575; }}
            .status-cell {{ text-align: center; }}
            .footer {{ margin-top: 30px; border-top: 1px solid #ccc; padding-top: 15px; font-size: 9pt; color: #666; }}
            .signoff {{ margin-top: 40px; }}
            .signoff-line {{ border-bottom: 1px solid #333; width: 250px; display: inline-block; margin: 0 20px; }}
            @page {{ @bottom-right {{ content: "Page " counter(page) " of " counter(pages); font-size: 8pt; }} }}
        </style>
    </head>
    <body>
        <h1>{client_name}<br/><small style="font-size:14pt;color:#666;">{project_name} — Punch List</small></h1>
        
        <div class="summary-box">
            <strong>Date:</strong> {datetime.utcnow().strftime('%B %d, %Y')}&nbsp;&nbsp;|&nbsp;&nbsp;
            <strong>Superintendent:</strong> {metadata.get('superintendent', 'N/A') if metadata else 'N/A'}&nbsp;&nbsp;|&nbsp;&nbsp;
            <strong>Total Items:</strong> {len(items)}&nbsp;&nbsp;|&nbsp;&nbsp;
            <span style="color:#d32f2f;">Life Safety: {priority_counts.get('Life Safety', 0)}</span>
            <span style="color:#e65100;">High: {priority_counts.get('High', 0)}</span>
            <span style="color:#f57f17;">Medium: {priority_counts.get('Medium', 0)}</span>
            <span style="color:#2e7d32;">Low: {priority_counts.get('Low', 0)}</span>
            <span style="color:#757575;">Cosmetic: {priority_counts.get('Cosmetic', 0)}</span>
        </div>
        
        {trade_sections}
        
        <div class="signoff">
            <p><strong>Reviewed and Approved By:</strong></p>
            <p>Superintendent: <span class="signoff-line"></span> Date: <span class="signoff-line" style="width:120px;"></span></p>
            <p>Project Manager: <span class="signoff-line"></span> Date: <span class="signoff-line" style="width:120px;"></span></p>
        </div>
        
        <div class="footer">
            <p>Generated by AI transcription system — reviewed and approved by superintendent prior to distribution.</p>
            <p>Walkthrough ID: {walkthrough_id} | Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')}</p>
        </div>
    </body>
    </html>"""
    
    # Generate PDF
    pdf_bytes = HTML(string=html_content).write_pdf()
    
    # Upload to S3
    pdf_key = f'reports/{walkthrough_id}_punch_list.pdf'
    bucket = os.environ.get('BUCKET_NAME', 'client-name-walkthrough-audio')
    s3.put_object(
        Bucket=bucket,
        Key=pdf_key,
        Body=pdf_bytes,
        ContentType='application/pdf'
    )
    
    # Generate pre-signed URL for download (valid 7 days)
    download_url = s3.generate_presigned_url(
        'get_object',
        Params={'Bucket': bucket, 'Key': pdf_key},
        ExpiresIn=604800
    )
    
    return {
        'statusCode': 200,
        'body': json.dumps({
            'pdf_key': pdf_key,
            'download_url': download_url,
            'total_items': len(items),
            'trades': len(by_trade)
        })
    }

Web Upload and Review Dashboard

Type: workflow Single-page web application that serves as the field upload interface and office review dashboard. Superintendents use it to upload walkthrough audio and review/approve extracted items. Project managers use it to generate PDF reports and push items to Fieldwire/Procore. Hosted as static files on S3+CloudFront with API Gateway backend.

Implementation

index.html
html
<!-- Main dashboard. Deploy to S3 bucket as static website. -->

<!-- index.html - Main dashboard -->
<!-- Deploy to S3 bucket as static website -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>Walkthrough Punch List</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; }
        .header { background: #1a1a2e; color: white; padding: 16px; display: flex; justify-content: space-between; align-items: center; }
        .header h1 { font-size: 18px; }
        .container { max-width: 800px; margin: 0 auto; padding: 16px; }
        
        /* Upload Section */
        .upload-zone { background: white; border: 3px dashed #ccc; border-radius: 12px; padding: 40px 20px; text-align: center; margin: 16px 0; cursor: pointer; transition: all 0.3s; }
        .upload-zone:hover, .upload-zone.dragover { border-color: #e94560; background: #fff5f7; }
        .upload-zone input[type="file"] { display: none; }
        .upload-btn { background: #e94560; color: white; border: none; padding: 16px 32px; border-radius: 8px; font-size: 18px; cursor: pointer; min-height: 56px; min-width: 200px; }
        .upload-btn:disabled { background: #ccc; }
        
        /* Project selector */
        select, input[type="text"] { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 16px; margin: 8px 0; }
        
        /* Walkthrough list */
        .walkthrough-card { background: white; border-radius: 8px; padding: 16px; margin: 12px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
        .walkthrough-card h3 { font-size: 16px; margin-bottom: 8px; }
        .status-badge { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: bold; }
        .status-processing { background: #fff3cd; color: #856404; }
        .status-review { background: #d4edda; color: #155724; }
        .status-approved { background: #cce5ff; color: #004085; }
        
        /* Punch item list */
        .punch-item { border-left: 4px solid #ccc; padding: 12px; margin: 8px 0; background: #fafafa; border-radius: 0 8px 8px 0; }
        .punch-item.priority-high, .punch-item.priority-life-safety { border-left-color: #d32f2f; }
        .punch-item.priority-medium { border-left-color: #f57f17; }
        .punch-item.priority-low, .punch-item.priority-cosmetic { border-left-color: #4caf50; }
        .punch-item .trade-tag { background: #e3f2fd; color: #1565c0; padding: 2px 8px; border-radius: 4px; font-size: 12px; }
        .punch-item .location { color: #666; font-size: 13px; }
        
        /* Action buttons - large touch targets for field use */
        .action-btn { min-height: 44px; min-width: 44px; padding: 10px 20px; border: none; border-radius: 8px; font-size: 14px; cursor: pointer; margin: 4px; }
        .btn-approve { background: #4caf50; color: white; }
        .btn-reject { background: #f44336; color: white; }
        .btn-edit { background: #2196f3; color: white; }
        .btn-export { background: #1a1a2e; color: white; }
        
        /* Progress bar */
        .progress { height: 8px; background: #e0e0e0; border-radius: 4px; overflow: hidden; margin: 8px 0; }
        .progress-bar { height: 100%; background: #4caf50; transition: width 0.5s; }
        
        /* Loading spinner */
        .spinner { border: 3px solid #f3f3f3; border-top: 3px solid #e94560; border-radius: 50%; width: 24px; height: 24px; animation: spin 1s linear infinite; display: inline-block; }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
    </style>
</head>
<body>
    <div class="header">
        <h1>🏗️ Punch List AI</h1>
        <span id="user-name">Superintendent</span>
    </div>
    
    <div class="container">
        <!-- Upload Section -->
        <div class="upload-zone" id="upload-zone" onclick="document.getElementById('file-input').click()">
            <input type="file" id="file-input" accept="audio/*,video/*" multiple>
            <p style="font-size: 24px; margin-bottom: 12px;">🎤</p>
            <p style="font-size: 16px; font-weight: bold;">Tap to upload walkthrough audio</p>
            <p style="color: #999; margin-top: 8px;">M4A, MP3, WAV, MP4 supported</p>
        </div>
        
        <select id="project-select">
            <option value="">Select Project...</option>
            <!-- Populated dynamically from API -->
        </select>
        
        <div id="upload-progress" style="display:none;">
            <p>Uploading... <span class="spinner"></span></p>
            <div class="progress"><div class="progress-bar" id="progress-bar"></div></div>
        </div>
        
        <!-- Walkthroughs List -->
        <h2 style="margin: 20px 0 12px; font-size: 18px;">Recent Walkthroughs</h2>
        <div id="walkthroughs-list">
            <!-- Populated dynamically -->
        </div>
    </div>
    
    <script>
        const API_BASE = '{{API_GATEWAY_URL}}'; // Replace during deployment
        const BUCKET = '{{S3_BUCKET}}'; // Replace during deployment
        
        // File upload handler
        document.getElementById('file-input').addEventListener('change', async (e) => {
            const files = e.target.files;
            const project = document.getElementById('project-select').value;
            if (!project) { alert('Please select a project first'); return; }
            
            for (const file of files) {
                await uploadFile(file, project);
            }
        });
        
        // Drag and drop
        const zone = document.getElementById('upload-zone');
        zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('dragover'); });
        zone.addEventListener('dragleave', () => zone.classList.remove('dragover'));
        zone.addEventListener('drop', async (e) => {
            e.preventDefault(); zone.classList.remove('dragover');
            const project = document.getElementById('project-select').value;
            if (!project) { alert('Please select a project first'); return; }
            for (const file of e.dataTransfer.files) { await uploadFile(file, project); }
        });
        
        async function uploadFile(file, project) {
            document.getElementById('upload-progress').style.display = 'block';
            
            // Get pre-signed upload URL from API
            const urlResp = await fetch(`${API_BASE}/upload-url`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ filename: file.name, project: project, content_type: file.type })
            });
            const { upload_url, walkthrough_id } = await urlResp.json();
            
            // Upload to S3
            const xhr = new XMLHttpRequest();
            xhr.upload.onprogress = (e) => {
                if (e.lengthComputable) {
                    document.getElementById('progress-bar').style.width = (e.loaded/e.total*100) + '%';
                }
            };
            xhr.onload = () => {
                document.getElementById('upload-progress').style.display = 'none';
                alert('Upload complete! Processing will take 2-5 minutes.');
                loadWalkthroughs();
            };
            xhr.open('PUT', upload_url);
            xhr.setRequestHeader('Content-Type', file.type);
            xhr.send(file);
        }
        
        async function loadWalkthroughs() {
            const resp = await fetch(`${API_BASE}/walkthroughs`);
            const walkthroughs = await resp.json();
            
            const list = document.getElementById('walkthroughs-list');
            list.innerHTML = walkthroughs.map(w => `
                <div class="walkthrough-card">
                    <h3>${w.project_id} — ${new Date(w.timestamp).toLocaleDateString()}</h3>
                    <span class="status-badge status-${w.status === 'pending_review' ? 'review' : w.status}">${w.status.replace('_', ' ')}</span>
                    <span style="float:right;">${w.total_items} items</span>
                    <p style="margin-top:8px;color:#666;">By: ${w.superintendent}</p>
                    ${w.status === 'pending_review' ? `<button class="action-btn btn-approve" onclick="reviewWalkthrough('${w.walkthrough_id}')">Review Items →</button>` : ''}
                    ${w.status === 'approved' ? `<button class="action-btn btn-export" onclick="exportPDF('${w.walkthrough_id}')">📄 Export PDF</button> <button class="action-btn btn-approve" onclick="pushToFieldwire('${w.walkthrough_id}')">Push to Fieldwire</button>` : ''}
                </div>
            `).join('');
        }
        
        async function reviewWalkthrough(id) {
            const resp = await fetch(`${API_BASE}/walkthroughs/${id}/items`);
            const items = await resp.json();
            // Render review interface with approve/reject/edit for each item
            // Implementation continues with item-level review UI
        }
        
        async function exportPDF(id) {
            const resp = await fetch(`${API_BASE}/walkthroughs/${id}/pdf`, { method: 'POST' });
            const { download_url } = await resp.json();
            window.open(download_url, '_blank');
        }
        
        async function pushToFieldwire(id) {
            if (!confirm('Push all approved items to Fieldwire?')) return;
            const resp = await fetch(`${API_BASE}/walkthroughs/${id}/push/fieldwire`, { method: 'POST' });
            const result = await resp.json();
            alert(`Created ${result.tasks_created} tasks in Fieldwire. ${result.errors} errors.`);
        }
        
        // Load data on page load
        loadWalkthroughs();
    </script>
</body>
</html>
Note

API Gateway routes needed (implement as Lambda functions behind API Gateway): - POST /upload-url → generates pre-signed S3 upload URL - GET /walkthroughs → lists walkthroughs from DynamoDB (filter by project) - GET /walkthroughs/{id}/items → gets punch items for a walkthrough - PUT /walkthroughs/{id}/items/{item_id} → update item (edit/approve/reject) - POST /walkthroughs/{id}/approve-all → bulk approve all items - POST /walkthroughs/{id}/pdf → trigger PDF generation, return download URL - POST /walkthroughs/{id}/push/fieldwire → push approved items to Fieldwire - POST /walkthroughs/{id}/push/procore → push approved items to Procore

Narration Coach Prompt

Type: prompt A reference prompt used during superintendent training to demonstrate optimal narration technique. Can also be used as a GPT-5.4 mini post-processing step to clean up poorly structured narrations before extraction.

Implementation

SYSTEM PROMPT (for transcript cleanup/enhancement before extraction)

You are a construction narration interpreter. You receive raw, unstructured walkthrough narration transcripts from construction superintendents. Your job is to reorganize and clarify the narration into a clean, structured format that preserves ALL original information while making location, trade, and issue boundaries clear.

1
Preserve every issue mentioned — do not drop or combine items
2
Clarify implicit locations (if the super says 'same room' or 'next unit', resolve to the explicit location)
3
Remove filler words, phone interruptions, off-topic conversation, and repeated false starts
4
Keep the superintendent's original descriptions — do not embellish or add information
5
Insert clear separators between distinct punch items
6
Format as a numbered list with structure: [#]. [Location] | [Trade if mentioned] | [Description]

Example input:

Example raw superintendent narration input
text
"OK so we're in uh unit 302 now, the kitchen... the faucet's dripping again, same as 301. And the, uh, hold on... *phone rings* ...yeah no I'll call you back... OK where was I. Right, the backsplash tile in the kitchen here has a crack, about 6 inches long, near the outlet. Oh and the outlet cover is missing too. Moving to the master bath..."

Example output:

Example structured narration output
text
1. Unit 302, Kitchen | Plumbing | Faucet dripping (same issue as Unit 301)
2. Unit 302, Kitchen | Tile | Backsplash tile cracked, approximately 6 inches long, near the electrical outlet
3. Unit 302, Kitchen | Electrical | Outlet cover plate missing (near backsplash)
4. Unit 302, Master Bathroom | [continues...]

SUPERINTENDENT TRAINING REFERENCE CARD

How to Narrate a Walkthrough for AI Punch List

Start Each Area

Say the FULL location when you enter a new space:

  • ✅ "Unit 405, Master Bathroom"
  • ✅ "Building B, Second Floor, Hallway"
  • ❌ "OK next room" (AI can't determine location)

For Each Issue, Include

1
Trade (if you know it): "Plumbing", "Electrical", "Drywall", "Paint", etc.
2
Priority (optional): "High priority", "Low priority", "Life safety"
3
Description: What's wrong and where exactly
4
Action (optional): "Needs replacement", "Touch up required"

Example Narrations

  • ✅ "Unit 301, Kitchen. Plumbing. High priority. Hot water supply line has active drip at shut-off valve below vanity sink. Needs replacement before drywall close."
  • ✅ "Same unit, living room. Paint. Low priority. Two nail pops on north wall above the couch area, need to be filled and touched up."
  • ✅ "Moving to unit 302. Master bathroom. Tile. Grout crack along shower floor transition, approximately 8 inches. Medium priority."

Tips

  • Pause 2-3 seconds between items
  • Speak clearly but naturally — don't shout
  • Don't narrate directly next to running power tools
  • "Same unit" or "same room" is OK after you've established location
  • Numbers are fine: "third unit from left", "6-inch gap"
  • Start and stop recording at building entry/exit

Testing & Validation

DATA RETENTION TEST: Confirm S3 lifecycle configuration for raw audio expiration
bash
aws s3api get-bucket-lifecycle-configuration --bucket client-name-walkthrough-audio

Client Handoff

...

Client Handoff Checklist

Training Sessions (Schedule 2 sessions)

Session 1: Field Team Training (60 minutes, on-site)

  • Demonstrate PLAUD Note Pro operation: power on, start/stop recording, Bluetooth sync to phone, charging procedure
  • Practice structured narration technique with live walkthrough of 2-3 rooms (use the Narration Coach reference card)
  • Show how to upload audio via the web dashboard on mobile (bookmark to home screen)
  • Review and approve punch items on the dashboard (demonstrate approve, reject, edit workflows)
  • Handle common scenarios: poor connectivity (offline recording), battery management, noisy areas (ISOtunes usage)
  • Distribute laminated quick reference cards to each superintendent
  • Each superintendent performs a supervised practice walkthrough of 5-10 minutes

Session 2: Office/PM Team Training (45 minutes, remote or office)

  • Dashboard walkthrough: viewing walkthroughs, reviewing items, bulk operations
  • PDF export process: generating, downloading, printing, distributing to trades
  • Fieldwire/Procore push: how to push approved items, verify in PM platform, handle errors
  • Accuracy review process: how to provide feedback that improves AI extraction over time
  • Reporting: walkthrough history, item counts by trade/priority, closeout tracking

Documentation Package (Leave Behind)

1
Quick Reference Card (laminated, one per superintendent): Device operation, narration technique, upload steps, emergency contacts
2
System Architecture Document: Diagram of all components, API keys location (in secure vault), AWS resource inventory
3
Admin Guide: How to add/remove users, create new projects, modify trade categories, adjust prompt settings
4
Compliance Binder: Recording consent signage templates, subcontractor addendum template, data retention policy, state-specific consent requirements
5
Troubleshooting Guide: Common issues (upload failures, poor transcription, missing items) with resolution steps
6
Vendor Contact Sheet: OpenAI support, AWS support, Fieldwire support, PLAUD support, MSP escalation contacts

Success Criteria Review (Review with client PM at 30-day mark)

Account Credentials and Access (Secure Handoff)

  • OpenAI API key (stored in AWS Secrets Manager, not shared directly)
  • AWS console access (IAM user for client admin, read-only)
  • Fieldwire API token and admin credentials
  • Dashboard URL and access code
  • S3 bucket name and region
  • All credentials documented in client's password vault (1Password, Bitwarden, etc.)

Maintenance

Ongoing Maintenance Plan

Weekly Tasks (15-30 minutes)

  • Review CloudWatch logs for Lambda function errors; investigate any failed transcriptions or extractions
  • Check OpenAI API usage dashboard for unexpected cost spikes (target: <$50/month for typical contractor)
  • Verify S3 storage growth is within expected range (~500MB-2GB/month per active project)
  • Review any superintendent-flagged accuracy issues and queue prompt adjustments if pattern emerges

Monthly Tasks (1-2 hours)

  • Accuracy Audit: Sample 5 random walkthroughs, compare AI-extracted items to superintendent corrections. Calculate trade accuracy, location accuracy, priority accuracy, recall, and precision. Document trends.
  • Prompt Optimization: If any accuracy metric drops below target thresholds (trade <85%, location <80%, recall <90%), analyze error patterns and adjust the GPT-5.4 mini extraction prompt. Deploy updated prompt to S3 config file.
  • Cost Review: Generate AWS billing report for S3, Lambda, DynamoDB, and API Gateway. Review OpenAI invoice. Total monthly infrastructure cost should be $20-$75; flag if exceeding $100.
  • Device Health Check: Verify all PLAUD Note Pro devices are charging properly, firmware is updated via companion app, and storage is not full. Replace any damaged devices.
  • Software Updates: Check for Fieldwire/Procore API changes, OpenAI model updates, and Lambda runtime updates. Apply non-breaking updates.

Quarterly Tasks (2-4 hours)

  • Client Business Review: Meet with client PM to review system utilization metrics, accuracy trends, time savings achieved, and any workflow improvement requests.
  • Compliance Audit: Verify recording consent signage is maintained at all active sites. Confirm data retention policies are being enforced (raw audio deleted after 90 days). Review any new state privacy law changes.
  • Model Evaluation: Test newer OpenAI models (e.g., future GPT-5.4 mini successors) against current extraction prompt using 10 saved test transcripts. If newer model shows >5% accuracy improvement, plan migration.
  • Disaster Recovery Test: Verify S3 backups, test DynamoDB point-in-time recovery, confirm Lambda functions can be redeployed from source code.
  • Hardware Lifecycle: Assess device condition, battery health, and accessory wear. Budget for replacements (PLAUD Note Pro battery degrades after ~18-24 months of daily use).

Annual Tasks

  • Contract Renewal: Review MSP service agreement, adjust pricing for any scope changes, update SLA terms.
  • Platform Migration Assessment: Evaluate whether client has outgrown the current solution tier and should upgrade (e.g., from custom pipeline to full Procore integration, or from single-super to multi-team deployment).
  • Security Audit: Rotate all API keys, review IAM permissions, audit S3 bucket policies, verify encryption at rest and in transit.

SLA Considerations

  • Processing SLA: Walkthrough audio uploaded during business hours (6am-6pm local) should be processed to reviewable items within 15 minutes. Off-hours uploads: within 1 hour.
  • Uptime SLA: Dashboard available 99.5% (allows ~3.6 hours downtime/month for maintenance). Processing pipeline available 99% during business hours.
  • Response Time SLA: Critical issues (system completely down, data loss): 1-hour response, 4-hour resolution. Non-critical (accuracy degradation, cosmetic UI issues): next business day response, 1-week resolution.
  • Escalation Path: Tier 1 (superintendent can't upload/review) → MSP helpdesk. Tier 2 (processing failures, accuracy issues) → MSP engineer. Tier 3 (infrastructure/API outage) → MSP + vendor support (OpenAI, AWS, Fieldwire).

Update/Retraining Triggers

  • Client adds new trade categories or changes location naming conventions → update extraction prompt immediately
  • Client switches PM platform (e.g., Fieldwire to Procore) → develop new integration (estimate 2-4 weeks)
  • OpenAI deprecates Whisper model or GPT-5.4 mini → migrate to successor model within 30 days of deprecation notice
  • Accuracy drops below 75% on any metric for 2 consecutive months → emergency prompt rewrite and narration technique retraining
  • New state privacy law affects client's operating jurisdiction → compliance review within 30 days of law effective date

Alternatives

SaaS-Only Approach (WalkPunch + BuildPass)

Instead of building a custom API pipeline, use WalkPunch (free tier) for walkthrough-to-punch-list conversion and BuildPass ($99-$149/month) for the full construction management workflow. No custom code, no AWS infrastructure, no Lambda functions. The superintendent uploads walkthrough audio/video directly to WalkPunch, which generates the trade-sorted punch list, and items are manually transferred or exported to the client's PM platform.

Native PM Platform Voice Features (Procore Quick Capture / Raken)

If the client already subscribes to Procore or Raken, enable and configure the native voice-capture and punch list features within those platforms rather than introducing a separate AI pipeline. Procore Quick Capture allows video recording with auto-transcription directly into punch items. Raken provides voice-to-text for daily logs and punch lists at $15-$49/user/month.

Dedicated Hardware Recorder with PLAUD Subscription

Use the PLAUD Note Pro as both the recording device and the AI processing platform. PLAUD's companion app includes AI transcription, summarization, and note extraction capabilities built in. This eliminates the need for any custom backend infrastructure—the superintendent records, the PLAUD app transcribes and summarizes, and the output is manually formatted into punch list items or emailed to the office for processing.

Enterprise Platform (OpenSpace + Procore)

Deepgram Nova-3 Real-Time Streaming Pipeline

Replace the batch Whisper transcription with Deepgram Nova-3 real-time streaming, generating punch list items in near-real-time as the superintendent narrates. Items appear on the superintendent's phone screen within seconds of being spoken, allowing immediate correction and approval during the walkthrough itself rather than as a post-walkthrough review step.

Want early access to the full toolkit?