Turn a roster in any form into a verified contact list: photos of sign-in sheets, screenshots, PDFs, and spreadsheets transcribed with band-crop discipline, E.164 phone formatting, vCard 4.0 output, and flag-dont-fix verification. Use when someone hands over a roster, sign-in sheet, class list, team sheet, or attendee list and wants clean contacts back as CSV and VCF.
name: "roster_to_contact_list" description: "Turn a roster in any form into a verified contact list: photos of sign-in sheets, screenshots, PDFs, and spreadsheets transcribed with band-crop discipline, E.164 phone formatting, vCard 4.0 output, and flag-dont-fix verification. Use when someone hands over a roster, sign-in sheet, class list, team sheet, or attendee list and wants clean contacts back as CSV and VCF."
Roster to Contact List
Purpose
Turn a messy roster into a contact list a person can trust and import. The hard part is not typing. The hard part is not inventing. One wrong digit in a phone number or one guessed letter in a name makes the whole list suspect, so this skill is built around transcription discipline and verification: transcribe in bands, flag anything uncertain instead of fixing it by feel, then read the finished list back against the source before anything ships.
Team neutral by design. It works for a sports team, a class, a crew, an event sign-in sheet, a club, or a work roster. No firm specific fields, no assumed country, no assumed system at the far end.
Workflow
- Intake and source triage. Name the source type before touching it: photo of a paper sheet, screenshot, PDF with a text layer, scanned PDF, spreadsheet, or CSV. Keep the original file untouched and work on a copy. Record three facts up front: who the list belongs to, what country the phone numbers are from (ask if it is not written anywhere), and what the output will be used for (import to a phone, a CRM, a mailing tool).
- Extract the raw text by source type.
- Spreadsheet or CSV: read it directly. Map columns, never retype values.
- PDF with a text layer: pull the text layer first. Scanned PDF: treat it like a photo and use step 3.
- Photo or screenshot: straighten and crop out borders and background first. Higher resolution and even light help, but preprocessing can also erase thin characters like periods and decimal points, so compare one band with and without preprocessing before committing to it.
- Band-crop transcription (photos and scans). Never transcribe a whole page in one pass. Divide the image into horizontal bands that follow the sheet: one band per ruled section, or three to five rows per band when there are no rules. Label bands A, B, C from the top. Work one band at a time, zoomed in until the handwriting fills the screen, and transcribe row by row, left to right. Give every row a source_row value like B3 (band B, third row in the band). That label is how a later check finds the exact spot on the page in seconds.
- Transcribe literally. Copy what is written, including odd spelling, nicknames, and punctuation in names. Do not expand initials, do not standardize names, do not fix a domain that looks misspelled. Where a character is genuinely ambiguous, write your best reading, add the marker
[?] right after it, and note the alternative in the flag column. Common traps to mark, not decide: 0 vs O, 1 vs l vs I, 5 vs S, 8 vs B, rn read as m, cl read as d, a handwritten 7 crossed or not, and @ written so it reads like a or 2.
- Build the intermediate CSV. One row per person, columns: name, phone, email, org, title, note, country_calling_code, source_row, flag. Headers, blank rows, and section titles on the sheet do not become contacts. Note each skipped line in the flag report so the row count in step 7 can be reconciled.
- Normalize phones to E.164, with no guessing. E.164 (ITU-T) format is a plus sign, a country code of 1 to 3 digits, then the national number, digits only, 15 digits maximum in total, and no country code starts with 0. Rules the builder applies:
- A number already starting with + keeps its digits.
- A leading 00 converts to +.
- A national number gets the country code from the row or the stated default, and a single leading trunk 0 is dropped (020 in London becomes +44 20...). The drop is recorded as a flag.
- No known country: the number is flagged
phone_no_country_code_known and left out of the vCard. Never default to +1 by habit.
- An extension is stripped from the number and flagged, not dialed. Structural checks catch bad shape only. For real validity, run the numbers through Google libphonenumber (Python port:
phonenumbers, parse with a region hint, then is_valid_number) when it is installed.
- Verify with a full read-back. This step is not optional. Read the finished CSV against the source, band by band, in a different order than you transcribed (bottom band up works well, it stops autopilot). Check phone digits and email characters one at a time, out loud in your head, not as whole strings. Check names letter by letter wherever a
[?] marker or a flag sits. Reconcile the counts: source rows equals contact rows plus skipped lines plus flagged unreadable rows, and every difference must have a written reason. A list that has not passed this step gets labeled UNVERIFIED in the handoff, plain as that.
- Build the outputs. Run the bundled builder:
python3 scripts/contact_builder.py roster.csv --outdir out Add --default-calling-code 44 (or per-row values) when the country is confirmed. It writes contacts.csv, contacts.vcf, and flags.json.
- Hand off with the flag report first. Lead with how many rows were flagged and why, then the counts from step 7, then the files. A person fixing three flagged rows by looking at the sheet is a good outcome. A list that hides its guesses is a bad one, however clean it looks.
Output Contract
Every job delivers three files plus a short report:
- contacts.csv: name, phone_e164, email, org, title, note, source_row, flags. One row per person, in source order.
- contacts.vcf: one vCard 4.0 card per named person (RFC 6350: BEGIN:VCARD, VERSION:4.0 immediately after, FN, N split as family and given, TEL as a tel: URI only when the number passed the E.164 structural check, EMAIL, ORG, TITLE, NOTE carrying any flags). Lines over 75 octets are folded with CRLF plus one space, and text values escape backslash, comma, semicolon, and newline.
- flags.json: total rows, flagged row count, and every flagged row with its reasons.
- The report: source type, band count if transcribed, row reconciliation from step 7, flag summary, and whether the read-back happened.
Worked example. Input rows:
name,phone,email,org,title,note,country_calling_code,source_row,flag
Alex Rivera,(415) 555-2671,alex@example.com,Harbor Crew,Lead,,1,A1,
Sam Okafor,020 7183 8750,sam.okafor@example.co.uk,,,,44,A2,
Jo Lee,555 0142,jo@exampl,Studio,,number looks short,,A3,handwriting faint
Builder output: Rivera becomes +14155552671 with a clean card. Okafor becomes +442071838750 with a trunk_zero_dropped flag. Lee is flagged phone_no_country_code_known and email_fails_basic_syntax, keeps no TEL or EMAIL line in the vCard, and appears in flags.json for a human to check against band A. Nothing was silently corrected.
Operating Rules
- Flag, do not fix. An ambiguous character, a suspect domain like gmial, a short number, a name that might be Jon or Jan: all get flags and stay as written. The owner of the list decides, with the sheet in front of them.
- Never invent a country code, a digit, an area code, or an email domain. A missing value is a flag. A guessed value is an error you shipped.
- Row counts must reconcile before delivery. An unreconciled count means a row was dropped or doubled, and that gets found before the user does.
- Duplicates are reported, not merged. Same phone or email on two rows gets a
possible_duplicate_of_row_N flag on the later row. Two people can share a phone, and one person can sign in twice. The report says which it might be, the user decides.
- Personal data discipline: a roster is other people's names, phones, and emails. Keep the files local, do not paste the list into random online OCR or converter sites, and do not include the list in messages to anyone but the person who supplied it.
- Handwriting with no legible reading at all: enter the row with whatever fields are readable, mark the rest
[?] in the flag column, and count it in the reconciliation as flagged unreadable. Never drop it silently.
- If the source is too degraded to transcribe honestly (blur, glare, cropped columns), say so and ask for a better copy instead of producing a confident list from a bad image.
- Sources behind the format claims: ITU-T E.164 via the Twilio glossary summary, RFC 6350 for vCard 4.0 (folding, escaping, FN and N, TEL TYPE values), and Google libphonenumber documentation for parse and validate behavior. Full URLs are in references/sources.md.
References
- references/sources.md for the verified source list.
- references/verification-checklist.md for the read-back checklist in a form you can copy into a job folder.
No ratings yet — be the first to rate it.